diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 54966fdfb0..1ce10fa6cd 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -8,7 +8,7 @@ env: BUILD_TYPE: RelWithDebInfo jobs: - build: + Ubuntu: runs-on: ubuntu-latest steps: @@ -26,25 +26,62 @@ jobs: key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} max-size: 1000M + - name: Install gtest + run: | + export CONFIGURATION="Release" + export GOOGLETEST_DIR="." + export GENERATOR="Unix Makefiles" + export CC="gcc" + export CXX="g++" + sudo -E CI/build_googletest.sh + - name: Configure - run: cmake -S . -B . -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS='-Werror' -DCMAKE_CXX_FLAGS="-Werror -Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" + run: cmake -S . -B . -DGTEST_ROOT="$(pwd)/googletest/build" -DGMOCK_ROOT="$(pwd)/googletest/build" -DBUILD_UNITTESTS=ON -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=./install -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_FLAGS='-Werror' -DCMAKE_CXX_FLAGS="-Werror -Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy" - name: Build run: cmake --build . --config ${{env.BUILD_TYPE}} --parallel 3 - - name: Install - shell: bash - run: cmake --install . + - name: Test + run: ./openmw_test_suite + +# - name: Install +# shell: bash +# run: cmake --install . - - name: Create Artifact - shell: bash - working-directory: install - run: | - ls -laR - 7z a ../build_artifact.7z . +# - name: Create Artifact +# shell: bash +# working-directory: install +# run: | +# ls -laR +# 7z a ../build_artifact.7z . - - name: Upload Artifact - uses: actions/upload-artifact@v1 +# - name: Upload Artifact +# uses: actions/upload-artifact@v1 +# with: +# path: ./build_artifact.7z +# name: build_artifact.7z + + MacOS: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Building Dependancies + run: CI/before_install.osx.sh + + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 with: - path: ./build_artifact.7z - name: build_artifact.7z + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M + + - name: Configure + run: | + rm -fr build # remove the build directory + CI/before_script.osx.sh + + - name: Build + run: | + cd build + make -j $(sysctl -n hw.logicalcpu) package diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9b8cf9341..538bfa11ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,14 +3,17 @@ stages: - build -.Debian_Image: +.Ubuntu_Image: tags: - docker - linux - image: debian:bullseye + image: ubuntu:focal + rules: + - if: $CI_PIPELINE_SOURCE == "push" + -.Debian: - extends: .Debian_Image +.Ubuntu: + extends: .Ubuntu_Image cache: paths: - apt-cache/ @@ -32,11 +35,12 @@ stages: - build/install/ Clang_Tidy: - extends: .Debian_Image + extends: .Ubuntu_Image stage: build rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' before_script: + - apt-get update; apt-get -y install software-properties-common; add-apt-repository -y ppa:openmw/openmw - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic clang-tidy clang script: - CI/before_script.linux.sh @@ -47,13 +51,17 @@ Clang_Tidy: CXX: clang++ CI_CLANG_TIDY: 1 timeout: 8h + artifacts: + paths: [] + expire_in: 1 minute Coverity: - extends: .Debian_Image + extends: .Ubuntu_Image stage: build rules: - - if: '$CI_PIPELINE_SOURCE == "schedule"' + - if: $CI_PIPELINE_SOURCE == "schedule" before_script: + - apt-get update; apt-get -y install software-properties-common; add-apt-repository -y ppa:openmw/openmw - 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 @@ -67,16 +75,20 @@ Coverity: --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" + - cat /builds/OpenMW/openmw/cov-int/build-log.txt variables: CC: gcc CXX: g++ artifacts: + paths: [] + expire_in: 1 minute -Debian_GCC: - extends: .Debian +Ubuntu_GCC: + extends: .Ubuntu cache: - key: Debian_GCC.v2 + key: Ubuntu_GCC.v2 before_script: + - apt-get update; apt-get -y install software-properties-common; add-apt-repository -y ppa:openmw/openmw - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: CC: gcc @@ -85,51 +97,62 @@ Debian_GCC: # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h -Debian_GCC_tests: - extends: Debian_GCC +Ubuntu_GCC_tests: + extends: Ubuntu_GCC cache: - key: Debian_GCC_tests.v2 + key: Ubuntu_GCC_tests.v2 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + expire_in: 1 minute -Debian_GCC_tests_Debug: - extends: Debian_GCC +Ubuntu_GCC_tests_Debug: + extends: Ubuntu_GCC cache: - key: Debian_GCC_tests_Debug.v1 + key: Ubuntu_GCC_tests_Debug.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 CMAKE_BUILD_TYPE: Debug + artifacts: + paths: [] + expire_in: 1 minute -Debian_GCC_Static_Deps: - extends: Debian_GCC +Ubuntu_GCC_Static_Deps: + extends: Ubuntu_GCC cache: - key: Debian_GCC_Static_Deps + key: Ubuntu_GCC_Static_Deps paths: - apt-cache/ - ccache/ - build/extern/fetched/ before_script: + - apt-get update; apt-get -y install software-properties-common; add-apt-repository -y ppa:openmw/openmw - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-static variables: CI_OPENMW_USE_STATIC_DEPS: 1 timeout: 3h -Debian_GCC_Static_Deps_tests: - extends: Debian_GCC_Static_Deps +Ubuntu_GCC_Static_Deps_tests: + extends: Ubuntu_GCC_Static_Deps cache: - key: Debian_GCC_Static_Deps_tests + key: Ubuntu_GCC_Static_Deps_tests variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + expire_in: 1 minute -Debian_Clang: - extends: .Debian +Ubuntu_Clang: + extends: .Ubuntu before_script: + - apt-get update; apt-get -y install software-properties-common; add-apt-repository -y ppa:openmw/openmw - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: - key: Debian_Clang.v2 + key: Ubuntu_Clang.v2 variables: CC: clang CXX: clang++ @@ -137,22 +160,28 @@ Debian_Clang: # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 2h -Debian_Clang_tests: - extends: Debian_Clang +Ubuntu_Clang_tests: + extends: Ubuntu_Clang cache: - key: Debian_Clang_tests.v2 + key: Ubuntu_Clang_tests.v2 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 + artifacts: + paths: [] + expire_in: 1 minute -Debian_Clang_tests_Debug: - extends: Debian_Clang +Ubuntu_Clang_tests_Debug: + extends: Ubuntu_Clang cache: - key: Debian_Clang_tests_Debug.v1 + key: Ubuntu_Clang_tests_Debug.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 CMAKE_BUILD_TYPE: Debug + artifacts: + paths: [] + expire_in: 1 minute .MacOS: image: macos-11-xcode-12 @@ -161,7 +190,7 @@ Debian_Clang_tests_Debug: stage: build only: variables: - - $CI_PROJECT_ID == "7107382" + - $CI_PROJECT_ID == "7107382" && $CI_PIPELINE_SOURCE == "push" cache: paths: - ccache/ @@ -174,7 +203,7 @@ Debian_Clang_tests_Debug: - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.osx.sh - cd build; make -j $(sysctl -n hw.logicalcpu) package - - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.dmg"; done + - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}_${CI_JOB_ID}.dmg"; done - ccache -s artifacts: paths: @@ -184,26 +213,17 @@ Debian_Clang_tests_Debug: macOS11_Xcode12: extends: .MacOS image: macos-11-xcode-12 - allow_failure: true cache: key: macOS11_Xcode12.v1 variables: CCACHE_SIZE: 3G -macOS10.15_Xcode11: - extends: .MacOS - image: macos-10.15-xcode-11 - cache: - key: macOS10.15_Xcode11.v1 - variables: - CCACHE_SIZE: 3G - variables: &engine-targets - targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" + targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard" package: "Engine" variables: &cs-targets - targets: "openmw-cs,bsatool,esmtool,niftest" + targets: "openmw-cs,bsatool,esmtool,niftest,openmw-essimporter" package: "CS" variables: &tests-targets @@ -213,6 +233,8 @@ variables: &tests-targets .Windows_Ninja_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -325,10 +347,15 @@ Windows_Ninja_Tests_RelWithDebInfo: config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + artifacts: + paths: [] + expire_in: 1 minute .Windows_MSBuild_Base: tags: - windows + rules: + - if: $CI_PIPELINE_SOURCE == "push" before_script: - Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolatey/" --priority=1 @@ -395,6 +422,10 @@ Windows_MSBuild_Engine_Release: variables: <<: *engine-targets config: "Release" + rules: + # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it + - if: $CI_PIPELINE_SOURCE == "push" + - if: $CI_PIPELINE_SOURCE == "schedule" Windows_MSBuild_Engine_Debug: extends: @@ -416,6 +447,10 @@ Windows_MSBuild_CS_Release: variables: <<: *cs-targets config: "Release" + rules: + # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it + - if: $CI_PIPELINE_SOURCE == "push" + - if: $CI_PIPELINE_SOURCE == "schedule" Windows_MSBuild_CS_Debug: extends: @@ -439,25 +474,28 @@ Windows_MSBuild_Tests_RelWithDebInfo: config: "RelWithDebInfo" # Gitlab can't successfully execute following binaries due to unknown reason # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" + artifacts: + paths: [] + expire_in: 1 minute -Debian_AndroidNDK_arm64-v8a: +Ubuntu_AndroidNDK_arm64-v8a: tags: - linux - image: debian:bullseye + image: psi29a/android-ndk:focal-ndk22 + rules: + - if: $CI_PIPELINE_SOURCE == "push" variables: CCACHE_SIZE: 3G cache: - key: Debian_AndroidNDK_arm64-v8a.v3 + key: Ubuntu__Focal_AndroidNDK_r22b_arm64-v8a.v1 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 + - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential stage: build script: - export CCACHE_BASEDIR="`pwd`" @@ -475,3 +513,18 @@ Debian_AndroidNDK_arm64-v8a: # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 1h30m +FindMissingMergeRequests: + image: python:latest + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: FindMissingMergeRequests.v1 + paths: + - .cache/pip + before_script: + - pip3 install --user requests click discord_webhook + script: + - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..e0b39ec495 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + diff --git a/.resubmitted_merge_requests.txt b/.resubmitted_merge_requests.txt new file mode 100644 index 0000000000..5fd67c190c --- /dev/null +++ b/.resubmitted_merge_requests.txt @@ -0,0 +1,7 @@ +1450 +1420 +1314 +1216 +1172 +1160 +1051 diff --git a/AUTHORS.md b/AUTHORS.md index 2080f12a99..67be756f33 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -31,6 +31,7 @@ Programmers Allofich Andreas Stöckel Andrei Kortunov (akortunov) + Andrew Appuhamy (andrew-app) AnyOldName3 Ardekantur Armin Preiml @@ -92,6 +93,7 @@ Programmers Haoda Wang (h313) hristoast Internecine + Ivan Beloborodov (myrix) Jackerty Jacob Essex (Yacoby) Jacob Turnbull (Tankinfrank) @@ -177,6 +179,7 @@ Programmers PlutonicOverkill Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) + Randy Davin (Kindi) rdimesio rexelion riothamus @@ -214,6 +217,7 @@ Programmers tlmullis tri4ng1e Thoronador + Tom Lowe (Vulpen) Tom Mason (wheybags) Torben Leif Carrington (TorbenC) unelsson @@ -229,7 +233,7 @@ Programmers Yuri Krupenin zelurker Noah Gooder - Andrew Appuhamy (andrew-app) + Documentation ------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ded2a9bf..984b545ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,29 @@ ------ Bug #1751: Birthsign abilities increase modified attribute values instead of base ones + Bug #1930: Followers are still fighting if a target stops combat with a leader Bug #3246: ESSImporter: Most NPCs are dead on save load + Bug #3488: AI combat aiming is too slow Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions) + Bug #3792: 1 frame late magicka recalc breaks early scripted magicka reactions to Intelligence change Bug #3846: Strings starting with "-" fail to compile if not enclosed in quotes + Bug #3855: AI sometimes spams defensive spells Bug #3905: Great House Dagoth issues Bug #4203: Resurrecting an actor should close the loot GUI + Bug #4376: Moved actors don't respawn in their original cells + Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node Bug #4602: Robert's Bodies: crash inside createInstance() Bug #4700: Editor: Incorrect command implementation Bug #4744: Invisible particles must still be processed + Bug #5088: Sky abruptly changes direction during certain weather transitions Bug #5100: Persuasion doesn't always clamp the resulting disposition Bug #5120: Scripted object spawning updates physics system Bug #5207: Loose summons can be present in scene + Bug #5377: console does not appear after using menutest in inventory Bug #5379: Wandering NPCs falling through cantons + Bug #5394: Windows snapping no longer works + Bug #5434: Pinned windows shouldn't cover breath progress bar Bug #5453: Magic effect VFX are offset for creatures Bug #5483: AutoCalc flag is not used to calculate spells cost Bug #5508: Engine binary links to Qt without using it @@ -25,6 +35,8 @@ Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention Bug #5842: GetDisposition adds temporary disposition change from different actors Bug #5863: GetEffect should return true after the player has teleported + Bug #5913: Failed assertion during Ritual of Trees quest + Bug #5937: Lights always need to be rotated by 90 degrees Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher Bug #6051: NaN water height in ESM file is not handled gracefully Bug #6066: addtopic "return" does not work from within script. No errors thrown @@ -40,19 +52,45 @@ Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player Bug #6143: Capturing a screenshot makes engine to be a temporary unresponsive Bug #6165: Paralyzed player character can pickup items when the inventory is open + Bug #6168: Weather particles flicker for a frame at start of storms Bug #6172: Some creatures can't open doors Bug #6174: Spellmaking and Enchanting sliders differences from vanilla + Bug #6177: Followers of player follower stop following after waiting for a day Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla Bug #6197: Infinite Casting Loop + Bug #6253: Multiple instances of Reflect stack additively + Bug #6255: Reflect is different from vanilla + Bug #6258: Barter menu glitches out when modifying prices Bug #6273: Respawning NPCs rotation is inconsistent Bug #6282: Laura craft doesn't follow the player character Bug #6283: Avis Dorsey follows you after her death + Bug #6285: Brush template drawing and terrain selection drawing performance is very bad Bug #6289: Keyword search in dialogues expected the text to be all ASCII characters Bug #6291: Can't pickup the dead mage's journal from the mysterious hunter mod Bug #6302: Teleporting disabled actor breaks its disabled state Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken + Bug #6321: Arrow enchantments should always be applied to the target + Bug #6322: Total sold/cost should reset to 0 when there are no items offered Bug #6323: Wyrmhaven: Alboin doesn't follower the player character out of his house + Bug #6324: Special Slave Companions: Can't buy the slave companions + Bug #6326: Detect Enchantment/Key should detect items in unresolved containers + Bug #6327: Blocking roots the character in place + Bug #6343: Magic projectile speed doesn't take race weight into account + Bug #6347: PlaceItem/PlaceItemCell/PlaceAt should work with levelled creatures + Bug #6354: SFX abruptly cut off after crossing max distance; implement soft fading of sound effects + Bug #6358: Changeweather command does not report an error when entering non-existent region + Bug #6363: Some scripts in Morrowland fail to work + Bug #6376: Creatures should be able to use torches + Bug #6386: Artifacts in water reflection due to imprecise screen-space coordinate computation + Bug #6396: Inputting certain Unicode characters triggers an assertion + Bug #6416: Morphs are applied to the wrong target + Bug #6417: OpenMW doesn't always use the right node to accumulate movement + Bug #6429: Wyrmhaven: Can't add AI packages to player + Bug #6433: Items bound to Quick Keys sometimes do not appear until the Quick Key menu is opened + Bug #6451: Weapon summoned from Cast When Used item will have the name "None" + Bug #6473: Strings from NIF should be parsed only to first null terminator Feature #890: OpenMW-CS: Column filtering + Feature #1465: "Reset" argument for AI functions Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record Feature #2780: A way to see current OpenMW version in the console Feature #3616: Allow Zoom levels on the World Map @@ -66,15 +104,20 @@ Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving Feature #6032: Reverse-z depth buffer + Feature #6078: First person should not clear depth buffer + Feature #6128: Soft Particles + Feature #6161: Refactor Sky to use shaders and GLES/GL3 friendly Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly Feature #6199: Support FBO Rendering + Feature #6248: Embedded error marker mesh Feature #6249: Alpha testing support for Collada Feature #6251: OpenMW-CS: Set instance movement based on camera zoom Feature #6288: Preserve the "blocked" record flag for referenceable objects. + Feature #6380: Commas are treated as whitespace in vanilla + Feature #6419: Topics shouldn't be greyed out if they can produce another topic reference Task #6201: Remove the "Note: No relevant classes found. No output generated" warnings Task #6264: Remove the old classes in animation.cpp - 0.47.0 ------ @@ -208,6 +251,8 @@ Bug #6043: Actor can have torch missing when torch animation is played Bug #6047: Mouse bindings can be triggered during save loading Bug #6136: Game freezes when NPCs try to open doors that are about to be closed + Bug #6142: Groundcover plugins change cells flags + Bug #6276: Deleted groundcover instances are not deleted in game Bug #6294: Game crashes with empty pathgrid Feature #390: 3rd person look "over the shoulder" Feature #832: OpenMW-CS: Handle deleted references diff --git a/CI/before_install.android.sh b/CI/before_install.android.sh index 59d98f48c4..712ded2769 100755 --- a/CI/before_install.android.sh +++ b/CI/before_install.android.sh @@ -1,4 +1,4 @@ #!/bin/sh -ex -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201230.zip -o ~/openmw-android-deps.zip -unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20211114.zip -o ~/openmw-android-deps.zip +unzip -o ~/openmw-android-deps -d /android-ndk-r22/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh index 3ea429f1bb..bdf7d4a244 100755 --- a/CI/before_script.android.sh +++ b/CI/before_script.android.sh @@ -7,9 +7,10 @@ mkdir -p build cd build cmake \ --DCMAKE_TOOLCHAIN_FILE=/usr/lib/android-sdk/ndk-bundle/build/cmake/android.toolchain.cmake \ +-DCMAKE_TOOLCHAIN_FILE=/android-ndk-r22/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ +-DANDROID_LD=deprecated \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DCMAKE_INSTALL_PREFIX=install \ @@ -22,6 +23,5 @@ cmake \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ -DOPENMW_USE_SYSTEM_MYGUI=OFF \ --DOPENMW_USE_SYSTEM_OSG=OFF \ --DOPENMW_USE_SYSTEM_BULLET=OFF \ +-DOPENMW_USE_SYSTEM_SQLITE3=OFF \ .. diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 5d20fa75ce..b9fed204e3 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -40,6 +40,7 @@ if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then -DOPENMW_USE_SYSTEM_MYGUI=OFF -DOPENMW_USE_SYSTEM_OSG=OFF -DOPENMW_USE_SYSTEM_BULLET=OFF + -DOPENMW_USE_SYSTEM_SQLITE3=OFF ) fi diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 0a6123505e..1341289335 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -1018,6 +1018,7 @@ echo echo "Setting up OpenMW build..." add_cmake_opts -DOPENMW_MP_BUILD=on add_cmake_opts -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" +add_cmake_opts -DOPENMW_USE_SYSTEM_SQLITE3=OFF if [ ! -z $CI ]; then case $STEP in components ) diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 265e05b8ee..6d0fe8c99e 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -19,6 +19,7 @@ cmake \ -D CMAKE_OSX_DEPLOYMENT_TARGET="10.14" \ -D CMAKE_BUILD_TYPE=RELEASE \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ +-D OPENMW_USE_SYSTEM_SQLITE3=OFF \ -D BUILD_OPENMW=TRUE \ -D BUILD_OPENCS=TRUE \ -D BUILD_ESMTOOL=TRUE \ diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 4b47c937da..a8843207d9 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -27,7 +27,7 @@ declare -rA GROUPED_DEPS=( # TODO: add librecastnavigation-dev when debian is ready # These dependencies can alternatively be built and linked statically. - [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev" + [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev libsqlite3-dev" [coverity]="curl" [clang-tidy]="clang-tidy" diff --git a/CMakeLists.txt b/CMakeLists.txt index 2564379847..644a7419a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,8 @@ else() endif() option(RECASTNAVIGATION_STATIC "Build recastnavigation static libraries" ${_recastnavigation_static_default}) +option(OPENMW_USE_SYSTEM_SQLITE3 "Use system provided SQLite3 library" ON) + option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE) option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) @@ -197,6 +199,10 @@ if (WIN32) option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON) endif() +if(MSVC) + add_compile_options("/utf-8") +endif() + # Dependencies find_package(OpenGL REQUIRED) @@ -408,7 +414,8 @@ else(USE_LUAJIT) endif(USE_LUAJIT) # C++ library binding to Lua -set(SOL_INCLUDE_DIRS ${OpenMW_SOURCE_DIR}/extern/sol3.2.2 ${OpenMW_SOURCE_DIR}/extern/sol_config) +set(SOL_INCLUDE_DIR ${OpenMW_SOURCE_DIR}/extern/sol3.2.2) +set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config) include_directories( BEFORE SYSTEM @@ -420,7 +427,8 @@ include_directories( ${OPENGL_INCLUDE_DIR} ${BULLET_INCLUDE_DIRS} ${LUA_INCLUDE_DIR} - ${SOL_INCLUDE_DIRS} + ${SOL_INCLUDE_DIR} + ${SOL_CONFIG_DIR} ) link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS}) @@ -437,9 +445,8 @@ if (APPLE) "${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY) endif (APPLE) -if (NOT APPLE) - set(OPENMW_MYGUI_FILES_ROOT ${OpenMW_BINARY_DIR}) - set(OPENMW_SHADERS_ROOT ${OpenMW_BINARY_DIR}) +if (NOT APPLE) # this is modified for macOS use later in "apps/open[mw|cs]/CMakeLists.txt" + set(OPENMW_RESOURCES_ROOT ${OpenMW_BINARY_DIR}) endif () add_subdirectory(files/) @@ -703,8 +710,12 @@ if (WIN32) endif() if (BUILD_OPENMW AND APPLE) - # Without these flags LuaJit crashes on startup on OSX - set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") + if (USE_LUAJIT) + # Without these flags LuaJit crashes on startup on OSX + set_target_properties(openmw PROPERTIES LINK_FLAGS "-pagezero_size 10000 -image_base 100000000") + endif(USE_LUAJIT) + target_compile_definitions(components PRIVATE GL_SILENCE_DEPRECATION=1) + target_compile_definitions(openmw PRIVATE GL_SILENCE_DEPRECATION=1) endif() # Apple bundling diff --git a/README.md b/README.md index 638801b129..557cd614cd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ OpenMW ====== -[![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) [![pipeline status](https://gitlab.com/OpenMW/openmw/badges/master/pipeline.svg)](https://gitlab.com/OpenMW/openmw/commits/master) - OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index 6c530db41c..9d41b62597 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -25,18 +25,10 @@ namespace }; template - TilePosition generateTilePosition(int max, Random& random) + osg::Vec2i generateVec2i(int max, Random& random) { std::uniform_int_distribution distribution(0, max); - return TilePosition(distribution(random), distribution(random)); - } - - template - TileBounds generateTileBounds(Random& random) - { - std::uniform_real_distribution distribution(0.0, 1.0); - const osg::Vec2f min(distribution(random), distribution(random)); - return TileBounds {min, min + osg::Vec2f(1.0, 1.0)}; + return osg::Vec2i(distribution(random), distribution(random)); } template @@ -91,8 +83,7 @@ namespace { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { - const osg::Vec3f shift(distribution(random), distribution(random), distribution(random)); - return Cell {1, shift}; + return CellWater {generateVec2i(1000, random), Water {ESM::Land::REAL_SIZE, distribution(random)}}; }); } @@ -117,16 +108,18 @@ namespace { std::uniform_real_distribution distribution(0.0, 1.0); Heightfield result; - result.mBounds = generateTileBounds(random); + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; result.mMinHeight = distribution(random); result.mMaxHeight = result.mMinHeight + 1.0; - result.mShift = osg::Vec3f(distribution(random), distribution(random), distribution(random)); - result.mScale = distribution(random); result.mLength = static_cast(ESM::Land::LAND_SIZE); std::generate_n(std::back_inserter(result.mHeights), ESM::Land::LAND_NUM_VERTS, [&] { return distribution(random); }); + result.mOriginalSize = ESM::Land::LAND_SIZE; + result.mMinX = 0; + result.mMinY = 0; return result; } @@ -135,7 +128,8 @@ namespace { std::uniform_real_distribution distribution(0.0, 1.0); FlatHeightfield result; - result.mBounds = generateTileBounds(random); + result.mCellPosition = generateVec2i(1000, random); + result.mCellSize = ESM::Land::REAL_SIZE; result.mHeight = distribution(random); return result; } @@ -144,11 +138,11 @@ namespace Key generateKey(std::size_t triangles, Random& random) { const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random); - const TilePosition tilePosition = generateTilePosition(10000, random); + const TilePosition tilePosition = generateVec2i(10000, random); const std::size_t generation = std::uniform_int_distribution(0, 100)(random); const std::size_t revision = std::uniform_int_distribution(0, 10000)(random); Mesh mesh = generateMesh(triangles, random); - std::vector water; + std::vector water; generateWater(std::back_inserter(water), 1, random); RecastMesh recastMesh(generation, revision, std::move(mesh), std::move(water), {generateHeightfield(random)}, {generateFlatHeightfield(random)}); diff --git a/apps/bsatool/bsatool.cpp b/apps/bsatool/bsatool.cpp index 8e8cf89186..4057d627c4 100644 --- a/apps/bsatool/bsatool.cpp +++ b/apps/bsatool/bsatool.cpp @@ -233,7 +233,17 @@ int extract(std::unique_ptr& bsa, Arguments& info) std::string extractPath = info.extractfile; Misc::StringUtils::replaceAll(extractPath, "\\", "/"); - if (!bsa->exists(archivePath.c_str())) + Files::IStreamPtr stream; + // Get a stream for the file to extract + for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) + { + if (Misc::StringUtils::ciEqual(std::string(it->name()), archivePath)) + { + stream = bsa->getFile(&*it); + break; + } + } + if (!stream) { std::cout << "ERROR: file '" << archivePath << "' not found\n"; std::cout << "In archive: " << info.filename << std::endl; @@ -260,9 +270,6 @@ int extract(std::unique_ptr& bsa, Arguments& info) return 3; } - // Get a stream for the file to extract - Files::IStreamPtr stream = bsa->getFile(archivePath.c_str()); - bfs::ofstream out(target, std::ios::binary); // Write the file to disk @@ -296,8 +303,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); bfs::ofstream out(target, std::ios::binary); // Write the file to disk diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index bf5f47a76d..a0ee654da9 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -367,10 +367,10 @@ int load(Arguments& info) EsmTool::RecordBase *record = EsmTool::RecordBase::create(n); if (record == nullptr) { - if (skipped.count(n.intval) == 0) + if (skipped.count(n.toInt()) == 0) { std::cout << "Skipping " << n.toString() << " records.\n"; - skipped.emplace(n.intval); + skipped.emplace(n.toInt()); } esm.skipRecord(); @@ -402,7 +402,7 @@ int load(Arguments& info) record->print(); } - if (record->getType().intval == ESM::REC_CELL && loadCells && interested) + if (record->getType().toInt() == ESM::REC_CELL && loadCells && interested) { loadCell(record->cast()->get(), esm, info); } @@ -415,7 +415,7 @@ int load(Arguments& info) { delete record; } - ++info.data.mRecordStats[n.intval]; + ++info.data.mRecordStats[n.toInt()]; } } catch(std::exception &e) { @@ -459,7 +459,7 @@ int clone(Arguments& info) for (std::pair stat : info.data.mRecordStats) { ESM::NAME name; - name.intval = stat.first; + name = stat.first; int amount = stat.second; std::cout << std::setw(digitCount) << amount << " " << name.toString() << " "; if (++i % 3 == 0) @@ -496,7 +496,7 @@ int clone(Arguments& info) esm.startRecord(typeName.toString(), record->getFlags()); record->save(esm); - if (typeName.intval == ESM::REC_CELL) { + if (typeName.toInt() == ESM::REC_CELL) { ESM::Cell *ptr = &record->cast()->get(); if (!info.data.mCellRefs[ptr].empty()) { diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 55170e232e..171f64eabe 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -30,7 +30,7 @@ void printAIPackage(const ESM::AIPackage& p) { std::cout << " Travel Coordinates: (" << p.mTravel.mX << "," << p.mTravel.mY << "," << p.mTravel.mZ << ")" << std::endl; - std::cout << " Travel Unknown: " << p.mTravel.mUnk << std::endl; + std::cout << " Should repeat: " << p.mTravel.mShouldRepeat << std::endl; } else if (p.mType == ESM::AI_Follow || p.mType == ESM::AI_Escort) { @@ -38,12 +38,12 @@ void printAIPackage(const ESM::AIPackage& p) << p.mTarget.mY << "," << p.mTarget.mZ << ")" << std::endl; std::cout << " Duration: " << p.mTarget.mDuration << std::endl; std::cout << " Target ID: " << p.mTarget.mId.toString() << std::endl; - std::cout << " Unknown: " << p.mTarget.mUnk << std::endl; + std::cout << " Should repeat: " << p.mTarget.mShouldRepeat << std::endl; } else if (p.mType == ESM::AI_Activate) { std::cout << " Name: " << p.mActivate.mName.toString() << std::endl; - std::cout << " Activate Unknown: " << p.mActivate.mUnk << std::endl; + std::cout << " Should repeat: " << p.mActivate.mShouldRepeat << std::endl; } else { std::cout << " BadPackage: " << Misc::StringUtils::format("0x%08X", p.mType) << std::endl; @@ -176,7 +176,7 @@ RecordBase::create(const ESM::NAME type) { RecordBase *record = nullptr; - switch (type.intval) { + switch (type.toInt()) { case ESM::REC_ACTI: { record = new EsmTool::Record; diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 27f77e40d1..84f13b8c91 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -324,14 +324,14 @@ namespace ESSImport ESM::NAME n = esm.getRecName(); esm.getRecHeader(); - auto it = converters.find(n.intval); + auto it = converters.find(n.toInt()); if (it != converters.end()) { it->second->read(esm); } else { - if (unknownRecords.insert(n.intval).second) + if (unknownRecords.insert(n.toInt()).second) { std::ios::fmtflags f(std::cerr.flags()); std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset() << ")" << std::endl; diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 2c33992ac3..d09704851a 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -117,6 +117,11 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); loadSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); loadSettingBool(radialFogCheckBox, "radial fog", "Shaders"); + loadSettingBool(softParticlesCheckBox, "soft particles", "Shaders"); + loadSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders"); + if (Settings::Manager::getInt("antialiasing", "Video") == 0) { + antialiasAlphaTestCheckBox->setCheckState(Qt::Unchecked); + } loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool))); loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); @@ -207,7 +212,7 @@ bool Launcher::AdvancedPage::loadSettings() { // Saves loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - maximumQuicksavesComboBox->setValue(Settings::Manager::getInt("max quicksaves", "Saves")); + loadSettingInt(maximumQuicksavesComboBox,"max quicksaves", "Saves"); // Other Settings QString screenshotFormatString = QString::fromStdString(Settings::Manager::getString("screenshot format", "General")).toUpper(); @@ -252,14 +257,10 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); - int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); - if (unarmedFactorsStrengthIndex != Settings::Manager::getInt("strength influences hand to hand", "Game")) - Settings::Manager::setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); + saveSettingInt(unarmedFactorsStrengthComboBox, "strength influences hand to hand", "Game"); saveSettingBool(stealingFromKnockedOutCheckBox, "always allow stealing from knocked out actors", "Game"); saveSettingBool(enableNavigatorCheckBox, "enable", "Navigator"); - int numPhysicsThreads = physicsThreadsSpinBox->value(); - if (numPhysicsThreads != Settings::Manager::getInt("async num threads", "Physics")) - Settings::Manager::setInt("async num threads", "Physics", numPhysicsThreads); + saveSettingInt(physicsThreadsSpinBox, "async num threads", "Physics"); } // Visuals @@ -270,6 +271,8 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(autoUseTerrainSpecularMapsCheckBox, "auto use terrain specular maps", "Shaders"); saveSettingBool(bumpMapLocalLightingCheckBox, "apply lighting to environment maps", "Shaders"); saveSettingBool(radialFogCheckBox, "radial fog", "Shaders"); + saveSettingBool(softParticlesCheckBox, "soft particles", "Shaders"); + saveSettingBool(antialiasAlphaTestCheckBox, "antialias alpha test", "Shaders"); saveSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game"); saveSettingBool(animSourcesCheckBox, "use additional anim sources", "Game"); saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); @@ -349,9 +352,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); saveSettingBool(changeDialogTopicsCheckBox, "color topic enable", "GUI"); - int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); - if (showOwnedCurrentIndex != Settings::Manager::getInt("show owned", "Game")) - Settings::Manager::setInt("show owned", "Game", showOwnedCurrentIndex); + saveSettingInt(showOwnedComboBox,"show owned", "Game"); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); saveSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map"); saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); @@ -370,11 +371,7 @@ void Launcher::AdvancedPage::saveSettings() { // Saves Settings saveSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); - int maximumQuicksaves = maximumQuicksavesComboBox->value(); - if (maximumQuicksaves != Settings::Manager::getInt("max quicksaves", "Saves")) - { - Settings::Manager::setInt("max quicksaves", "Saves", maximumQuicksaves); - } + saveSettingInt(maximumQuicksavesComboBox, "max quicksaves", "Saves"); // Other Settings std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); @@ -416,6 +413,32 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str Settings::Manager::setBool(setting, group, cValue); } +void Launcher::AdvancedPage::loadSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group) +{ + int currentIndex = Settings::Manager::getInt(setting, group); + comboBox->setCurrentIndex(currentIndex); +} + +void Launcher::AdvancedPage::saveSettingInt(QComboBox *comboBox, const std::string &setting, const std::string &group) +{ + int currentIndex = comboBox->currentIndex(); + if (currentIndex != Settings::Manager::getInt(setting, group)) + Settings::Manager::setInt(setting, group, currentIndex); +} + +void Launcher::AdvancedPage::loadSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group) +{ + int value = Settings::Manager::getInt(setting, group); + spinBox->setValue(value); +} + +void Launcher::AdvancedPage::saveSettingInt(QSpinBox *spinBox, const std::string &setting, const std::string &group) +{ + int value = spinBox->value(); + if (value != Settings::Manager::getInt(setting, group)) + Settings::Manager::setInt(setting, group, value); +} + void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) { loadCellsForAutocomplete(cellNames); diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index 9685dcefeb..1d16fae706 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -41,8 +41,12 @@ namespace Launcher * @param filePaths the file paths of the content files to be examined */ void loadCellsForAutocomplete(QStringList filePaths); - void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); - void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); + static void loadSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group); + static void saveSettingInt(QComboBox *comboBox, const std::string& setting, const std::string& group); + static void loadSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group); + static void saveSettingInt(QSpinBox *spinBox, const std::string& setting, const std::string& group); }; } #endif diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 24729d5096..eb0950dae0 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -32,7 +32,7 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: { ui.setupUi (this); setObjectName ("DataFilesPage"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/true); const QString encoding = mGameSettings.value("encoding", "win1252"); mSelector->setEncoding(encoding); @@ -121,6 +121,7 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName) for (const QString &path : paths) mSelector->addFiles(path); + mSelector->sortFiles(); PathIterator pathIterator(paths); diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 8359834ddb..e6d857a943 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -47,7 +47,6 @@ Launcher::GraphicsPage::GraphicsPage(QWidget *parent) connect(screenComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(screenChanged(int))); connect(framerateLimitCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotFramerateLimitToggled(bool))); connect(shadowDistanceCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotShadowDistLimitToggled(bool))); - } bool Launcher::GraphicsPage::setupSDL() diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 75d4464b68..d335d7cded 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -146,7 +146,6 @@ void Launcher::MainDialog::createPages() connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); - } Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp index e7f6e83e74..e8ba54651c 100644 --- a/apps/launcher/utils/cellnameloader.cpp +++ b/apps/launcher/utils/cellnameloader.cpp @@ -10,6 +10,8 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) // Loop through all content files for (auto &contentPath : contentPaths) { + if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) + continue; esmReader.open(contentPath.toStdString()); // Loop through all records @@ -35,7 +37,7 @@ QSet CellNameLoader::getCellNames(QStringList &contentPaths) bool CellNameLoader::isCellRecord(ESM::NAME &recordName) { - return recordName.intval == ESM::REC_CELL; + return recordName.toInt() == ESM::REC_CELL; } QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index cb6205ef5c..c42df4a423 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -2,8 +2,8 @@ #include #include -#include +#include #include #include #include @@ -18,18 +18,10 @@ namespace bpo = boost::program_options; namespace bfs = boost::filesystem; ///See if the file has the named extension -bool hasExtension(std::string filename, std::string extensionToFind) +bool hasExtension(std::string filename, std::string extensionToFind) { std::string extension = filename.substr(filename.find_last_of('.')+1); - - //Convert strings to lower case for comparison - std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - std::transform(extensionToFind.begin(), extensionToFind.end(), extensionToFind.begin(), ::tolower); - - if(extension == extensionToFind) - return true; - else - return false; + return Misc::StringUtils::ciEqual(extension, extensionToFind); } ///See if the file has the "nif" extension. diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 952bbbdbda..b3d405d1a3 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -183,8 +183,7 @@ if(APPLE) set(OPENCS_BUNDLE_NAME "OpenMW-CS") set(OPENCS_BUNDLE_RESOURCES_DIR "${OpenMW_BINARY_DIR}/${OPENCS_BUNDLE_NAME}.app/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index ac0a7a5ef7..862d1c545f 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -149,11 +149,7 @@ std::pair > CS::Editor::readConfi dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end()); //iterate the data directories and add them to the file dialog for loading - for (Files::PathContainer::const_reverse_iterator iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) - { - QString path = QString::fromUtf8 (iter->string().c_str()); - mFileDialog.addFiles(path); - } + mFileDialog.addFiles(dataDirs); return std::make_pair (dataDirs, variables["fallback-archive"].as>()); } diff --git a/apps/opencs/model/filter/parser.cpp b/apps/opencs/model/filter/parser.cpp index d363b4849d..6f185d60fa 100644 --- a/apps/opencs/model/filter/parser.cpp +++ b/apps/opencs/model/filter/parser.cpp @@ -17,6 +17,19 @@ #include "textnode.hpp" #include "valuenode.hpp" +namespace +{ + bool isAlpha(char c) + { + return std::isalpha(static_cast(c)); + } + + bool isDigit(char c) + { + return std::isdigit(static_cast(c)); + } +} + namespace CSMFilter { struct Token @@ -103,7 +116,7 @@ CSMFilter::Token CSMFilter::Parser::getStringToken() { char c = mInput[mIndex]; - if (std::isalpha (c) || c==':' || c=='_' || (!string.empty() && std::isdigit (c)) || c=='"' || + if (isAlpha(c) || c==':' || c=='_' || (!string.empty() && isDigit(c)) || c=='"' || (!string.empty() && string[0]=='"')) string += c; else @@ -150,7 +163,7 @@ CSMFilter::Token CSMFilter::Parser::getNumberToken() { char c = mInput[mIndex]; - if (std::isdigit (c)) + if (isDigit(c)) { string += c; hasDigit = true; @@ -225,10 +238,10 @@ CSMFilter::Token CSMFilter::Parser::getNextToken() case '!': ++mIndex; return Token (Token::Type_OneShot); } - if (c=='"' || c=='_' || std::isalpha (c) || c==':') + if (c=='"' || c=='_' || isAlpha(c) || c==':') return getStringToken(); - if (c=='-' || c=='.' || std::isdigit (c)) + if (c=='-' || c=='.' || isDigit(c)) return getNumberToken(); error(); diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index fe9bc991d4..74643a46ee 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -395,12 +395,12 @@ bool CSMTools::TopicInfoCheckStage::verifyId(const std::string& name, const CSMW if (index == -1) { - messages.add(id, T::getRecordType() + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); + messages.add(id, std::string(T::getRecordType()) + " '" + name + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (collection.getRecord(index).isDeleted()) { - messages.add(id, "Deleted " + T::getRecordType() + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); + messages.add(id, "Deleted " + std::string(T::getRecordType()) + " record '" + name + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 6ab9d7ff9d..d15a4ff54d 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -296,7 +296,7 @@ namespace CSMWorld const std::string& destination, const UniversalId::Type type) { int index = cloneRecordImp(origin, destination, type); - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); } template @@ -311,7 +311,7 @@ namespace CSMWorld int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index)->get().mPlugin = 0; + mRecords.at(index)->get().setPlugin(0); return true; } diff --git a/apps/opencs/model/world/columnimp.cpp b/apps/opencs/model/world/columnimp.cpp index bec5008d35..0244ab1e8d 100644 --- a/apps/opencs/model/world/columnimp.cpp +++ b/apps/opencs/model/world/columnimp.cpp @@ -52,7 +52,7 @@ namespace CSMWorld QVariant LandPluginIndexColumn::get(const Record& record) const { - return record.get().mPlugin; + return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 6eefdb6e21..8e53b3b270 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -255,7 +255,7 @@ namespace CSMWorld { ColumnId_AiWanderDist, "Wander Dist" }, { ColumnId_AiDuration, "Ai Duration" }, { ColumnId_AiWanderToD, "Wander ToD" }, - { ColumnId_AiWanderRepeat, "Wander Repeat" }, + { ColumnId_AiWanderRepeat, "Ai Repeat" }, { ColumnId_AiActivateName, "Activate" }, { ColumnId_AiTargetId, "Target ID" }, { ColumnId_AiTargetCell, "Target Cell" }, diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 3723dac46f..ec4c1f6a13 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -1086,7 +1086,7 @@ bool CSMWorld::Data::continueLoading (CSMDoc::Messages& messages) bool unhandledRecord = false; - switch (n.intval) + switch (n.toInt()) { case ESM::REC_GLOB: mGlobals.load (*mReader, mBase); break; case ESM::REC_GMST: mGmsts.load (*mReader, mBase); break; diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 5b4a9b31bc..215f42133d 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -8,6 +8,7 @@ #include #include +#include #include "collectionbase.hpp" #include "columnbase.hpp" @@ -354,8 +355,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import for (int i = 0; i < idCollection()->getSize(); ++i) { auto& record = static_cast&>(idCollection()->getRecord(i)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture); if (record.isModified()) reverseLookupMap.emplace(texture, idCollection()->getId(i)); } @@ -376,8 +376,7 @@ CSMWorld::LandTextureIdTable::ImportResults CSMWorld::LandTextureIdTable::import // Look for a pre-existing record auto& record = static_cast&>(idCollection()->getRecord(oldRow)); - std::string texture = record.get().mTexture; - std::transform(texture.begin(), texture.end(), texture.begin(), tolower); + std::string texture = Misc::StringUtils::lowerCase(record.get().mTexture); auto searchIt = reverseLookupMap.find(texture); if (searchIt != reverseLookupMap.end()) { diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index c35d3c5a7c..153d1bde91 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -1678,7 +1678,7 @@ namespace CSMWorld newRow.mWander.mTimeOfDay = 0; for (int i = 0; i < 8; ++i) newRow.mWander.mIdle[i] = 0; - newRow.mWander.mShouldRepeat = 0; + newRow.mWander.mShouldRepeat = 1; newRow.mCellName = ""; if (position >= (int)list.size()) @@ -1784,9 +1784,15 @@ namespace CSMWorld return static_cast(content.mWander.mIdle[subColIndex-4]); else return QVariant(); - case 12: // wander repeat + case 12: // repeat if (content.mType == ESM::AI_Wander) return content.mWander.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Travel) + return content.mTravel.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + return content.mTarget.mShouldRepeat != 0; + else if (content.mType == ESM::AI_Activate) + return content.mActivate.mShouldRepeat != 0; else return QVariant(); case 13: // activate name @@ -1895,6 +1901,12 @@ namespace CSMWorld case 12: if (content.mType == ESM::AI_Wander) content.mWander.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Travel) + content.mTravel.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) + content.mTarget.mShouldRepeat = static_cast(value.toInt()); + else if (content.mType == ESM::AI_Activate) + content.mActivate.mShouldRepeat = static_cast(value.toInt()); else return; // return without saving diff --git a/apps/opencs/model/world/scriptcontext.cpp b/apps/opencs/model/world/scriptcontext.cpp index 344ae322e9..9b465d7ffb 100644 --- a/apps/opencs/model/world/scriptcontext.cpp +++ b/apps/opencs/model/world/scriptcontext.cpp @@ -102,11 +102,6 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const return std::binary_search (mIds.begin(), mIds.end(), Misc::StringUtils::lowerCase (name)); } -bool CSMWorld::ScriptContext::isJournalId (const std::string& name) const -{ - return mData.getJournals().searchId (name)!=-1; -} - void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; diff --git a/apps/opencs/model/world/scriptcontext.hpp b/apps/opencs/model/world/scriptcontext.hpp index 8e1a5e57b8..cb08fc70bd 100644 --- a/apps/opencs/model/world/scriptcontext.hpp +++ b/apps/opencs/model/world/scriptcontext.hpp @@ -39,9 +39,6 @@ namespace CSMWorld bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? - void invalidateIds(); void clear(); diff --git a/apps/opencs/view/doc/filedialog.cpp b/apps/opencs/view/doc/filedialog.cpp index 8ff063ed31..946dac047e 100644 --- a/apps/opencs/view/doc/filedialog.cpp +++ b/apps/opencs/view/doc/filedialog.cpp @@ -24,13 +24,18 @@ CSVDoc::FileDialog::FileDialog(QWidget *parent) : resize(400, 400); setObjectName ("FileDialog"); - mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget); + mSelector = new ContentSelectorView::ContentSelector (ui.contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget (this); } -void CSVDoc::FileDialog::addFiles(const QString &path) +void CSVDoc::FileDialog::addFiles(const std::vector& dataDirs) { - mSelector->addFiles(path); + for (auto iter = dataDirs.rbegin(); iter != dataDirs.rend(); ++iter) + { + QString path = QString::fromUtf8(iter->string().c_str()); + mSelector->addFiles(path); + } + mSelector->sortFiles(); } void CSVDoc::FileDialog::setEncoding(const QString &encoding) diff --git a/apps/opencs/view/doc/filedialog.hpp b/apps/opencs/view/doc/filedialog.hpp index 6c48fa78b9..6a15b46b7c 100644 --- a/apps/opencs/view/doc/filedialog.hpp +++ b/apps/opencs/view/doc/filedialog.hpp @@ -45,7 +45,7 @@ namespace CSVDoc explicit FileDialog(QWidget *parent = nullptr); void showDialog (ContentAction action); - void addFiles (const QString &path); + void addFiles(const std::vector& dataDirs); void setEncoding (const QString &encoding); void clearFiles (); diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index 271ca2365a..94b82c96cd 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -111,7 +111,7 @@ namespace CSVRender if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(mesh); - SceneUtil::attach(instance, mSkeleton, boneName, node->second); + SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index 789fad0587..9f4fd29966 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -308,7 +308,7 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) const float OuterRadius = InnerRadius + MarkerShaftWidth; const float SegmentDistance = 100.f; - const size_t SegmentCount = std::min(64, std::max(24, (int)(OuterRadius * 2 * osg::PI / SegmentDistance))); + const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; diff --git a/apps/opencs/view/render/terrainselection.cpp b/apps/opencs/view/render/terrainselection.cpp index 0593917e0a..1fc183ba47 100644 --- a/apps/opencs/view/render/terrainselection.cpp +++ b/apps/opencs/view/render/terrainselection.cpp @@ -16,11 +16,13 @@ #include "worldspacewidget.hpp" CSVRender::TerrainSelection::TerrainSelection(osg::Group* parentNode, WorldspaceWidget *worldspaceWidget, TerrainSelectionType type): -mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mDraggedOperationFlag(false), mSelectionType(type) +mParentNode(parentNode), mWorldspaceWidget (worldspaceWidget), mSelectionType(type) { mGeometry = new osg::Geometry(); mSelectionNode = new osg::Group(); + mSelectionNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mSelectionNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mSelectionNode->addChild(mGeometry); activate(); @@ -43,19 +45,24 @@ void CSVRender::TerrainSelection::onlySelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::AddSelect); + handleSelection(localPositions, SelectionMethod::AddSelect); } -void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::RemoveSelect); + handleSelection(localPositions, SelectionMethod::RemoveSelect); } -void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions, bool toggleInProgress) +void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions) { - handleSelection(localPositions, toggleInProgress, SelectionMethod::ToggleSelect); + handleSelection(localPositions, SelectionMethod::ToggleSelect); +} + +void CSVRender::TerrainSelection::clearTemporarySelection() +{ + mTemporarySelection.clear(); } void CSVRender::TerrainSelection::activate() @@ -102,26 +109,16 @@ void CSVRender::TerrainSelection::drawShapeSelection(const osg::ref_ptrpush_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1) + 2)); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y - 1), calculateLandHeight(x, y - 1))); vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y) + 2)); - - const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); - if (north == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1) + 2)); - } - - const auto east = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x + 1, y)); - if (east == mSelection.end()) - { - vertices->push_back(pointXY); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y) + 2)); - } + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x - 1), yWorldCoord, calculateLandHeight(x - 1, y))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(xWorldCoord, CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(y + 1), calculateLandHeight(x, y + 1))); + vertices->push_back(pointXY); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(x + 1), yWorldCoord, calculateLandHeight(x + 1, y))); } } } @@ -154,8 +151,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2)+2)); + vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+(i-1), y2))); + vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y + 1), calculateLandHeight(x1+i, y2))); } } @@ -166,8 +163,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1)+2)); - vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1)+2)); + vertices->push_back(osg::Vec3f(drawPreviousX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+(i-1), y1))); + vertices->push_back(osg::Vec3f(drawCurrentX, CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(y), calculateLandHeight(x1+i, y1))); } } @@ -178,8 +175,8 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i)+2)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawPreviousY, calculateLandHeight(x2, y1+(i-1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x + 1), drawCurrentY, calculateLandHeight(x2, y1+i))); } } @@ -190,103 +187,60 @@ void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptrpush_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1))+2)); - vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i)+2)); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawPreviousY, calculateLandHeight(x1, y1+(i-1)))); + vertices->push_back(osg::Vec3f(CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(x), drawCurrentY, calculateLandHeight(x1, y1+i))); } } } } } -void CSVRender::TerrainSelection::handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod) +void CSVRender::TerrainSelection::handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod) { - if (toggleInProgress) + for (auto const& localPos : localPositions) { - for (auto const& localPos : localPositions) - { - auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); - mDraggedOperationFlag = true; - - if (iterTemp == mTemporarySelection.end()) - { - auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); + const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); - switch (selectionMethod) - { - case SelectionMethod::AddSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - - break; - case SelectionMethod::RemoveSelect: - if (iter != mSelection.end()) - { - mSelection.erase(iter); - } - - break; - case SelectionMethod::ToggleSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - else - { - mSelection.erase(iter); - } - - break; - default: break; - } - } - - mTemporarySelection.push_back(localPos); - } - } - else if (mDraggedOperationFlag == false) - { - for (auto const& localPos : localPositions) + switch (selectionMethod) { - const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); + case SelectionMethod::OnlySelect: + break; - switch (selectionMethod) - { case SelectionMethod::AddSelect: if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } - break; + case SelectionMethod::RemoveSelect: if (iter != mSelection.end()) { mSelection.erase(iter); } - break; + case SelectionMethod::ToggleSelect: - if (iter == mSelection.end()) - { - mSelection.emplace_back(localPos); - } - else + { + const auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); + if (iterTemp == mTemporarySelection.end()) { - mSelection.erase(iter); + if (iter == mSelection.end()) + { + mSelection.emplace_back(localPos); + } + else + { + mSelection.erase(iter); + } } - + mTemporarySelection.emplace_back(localPos); break; - default: break; } - } - } - else - { - mDraggedOperationFlag = false; - mTemporarySelection.clear(); + default: + break; + } } update(); @@ -336,11 +290,9 @@ int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global ver else if (isLandLoaded(CSMWorld::CellCoordinates::generateId(cellX, cellY))) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); - CSMWorld::IdTable& landTable = dynamic_cast ( *document.getData().getTableModel (CSMWorld::UniversalId::Type_Land)); std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); - int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); - const CSMWorld::LandHeightsColumn::DataType mPointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)).value(); - return mPointer[localY*ESM::Land::LAND_SIZE + localX]; + const ESM::Land::LandData* landData = document.getData().getLand().getRecord(cellId).get().getLandData(ESM::Land::DATA_VHGT); + return landData->mHeights[localY*ESM::Land::LAND_SIZE + localX]; } return landHeight; diff --git a/apps/opencs/view/render/terrainselection.hpp b/apps/opencs/view/render/terrainselection.hpp index 18622ad13a..1d0da7bb59 100644 --- a/apps/opencs/view/render/terrainselection.hpp +++ b/apps/opencs/view/render/terrainselection.hpp @@ -44,9 +44,10 @@ namespace CSVRender ~TerrainSelection(); void onlySelect(const std::vector> &localPositions); - void addSelect(const std::vector>& localPositions, bool toggleInProgress); - void removeSelect(const std::vector>& localPositions, bool toggleInProgress); - void toggleSelect(const std::vector> &localPositions, bool toggleInProgress); + void addSelect(const std::vector>& localPositions); + void removeSelect(const std::vector>& localPositions); + void toggleSelect(const std::vector> &localPositions); + void clearTemporarySelection(); void activate(); void deactivate(); @@ -64,7 +65,7 @@ namespace CSVRender private: - void handleSelection(const std::vector>& localPositions, bool toggleInProgress, SelectionMethod selectionMethod); + void handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod); bool noCell(const std::string& cellId); @@ -73,7 +74,7 @@ namespace CSVRender bool noLandLoaded(const std::string& cellId); bool isLandLoaded(const std::string& cellId); - + osg::Group* mParentNode; WorldspaceWidget *mWorldspaceWidget; osg::ref_ptr mBaseNode; @@ -81,7 +82,6 @@ namespace CSVRender osg::ref_ptr mSelectionNode; std::vector> mSelection; // Global terrain selection coordinate in either vertex or texture units std::vector> mTemporarySelection; // Used during toggle to compare the most recent drag operation - bool mDraggedOperationFlag; //true during drag operation, false when click-operation TerrainSelectionType mSelectionType; }; } diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index 0504944954..936e8d6778 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -106,16 +106,6 @@ void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); applyTerrainEditChanges(); } - - if (mDragMode == InteractionType_PrimarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - } - - if (mDragMode == InteractionType_SecondarySelect) - { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - } } clearTransientEdits(); } @@ -124,7 +114,8 @@ void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult { if(hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + mTerrainShapeSelection->clearTemporarySelection(); } } @@ -132,7 +123,8 @@ void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResu { if(hit.hit && hit.tag == nullptr) { - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, false); + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + mTerrainShapeSelection->clearTemporarySelection(); } } @@ -167,8 +159,8 @@ bool CSVRender::TerrainShapeMode::primarySelectStartDrag (const QPoint& pos) mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); + return true; } bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) @@ -180,8 +172,8 @@ bool CSVRender::TerrainShapeMode::secondarySelectStartDrag (const QPoint& pos) mDragMode = InteractionType_None; return false; } - selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); - return false; + selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); + return true; } void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) @@ -200,13 +192,13 @@ void CSVRender::TerrainShapeMode::drag (const QPoint& pos, int diffX, int diffY, if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0, true); + if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1, true); + if (hit.hit && hit.tag == nullptr) selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); } } @@ -217,12 +209,17 @@ void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) applyTerrainEditChanges(); clearTransientEdits(); } + if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect) + { + mTerrainShapeSelection->clearTemporarySelection(); + } } void CSVRender::TerrainShapeMode::dragAborted() { clearTransientEdits(); + mDragMode = InteractionType_None; } void CSVRender::TerrainShapeMode::dragWheel (int diff, double speedFactor) @@ -1076,7 +1073,7 @@ void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int glob } } -void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; @@ -1136,11 +1133,11 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& if (selectAction == "Select only") mTerrainShapeSelection->onlySelect(selections); else if (selectAction == "Add to selection") - mTerrainShapeSelection->addSelect(selections, dragOperation); + mTerrainShapeSelection->addSelect(selections); else if (selectAction == "Remove from selection") - mTerrainShapeSelection->removeSelect(selections, dragOperation); + mTerrainShapeSelection->removeSelect(selections); else if (selectAction == "Invert selection") - mTerrainShapeSelection->toggleSelect(selections, dragOperation); + mTerrainShapeSelection->toggleSelect(selections); } void CSVRender::TerrainShapeMode::pushEditToCommand(const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp index abc4b8aba8..8b40483be4 100644 --- a/apps/opencs/view/render/terrainshapemode.hpp +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -142,7 +142,7 @@ namespace CSVRender void handleSelection(int globalSelectionX, int globalSelectionY, std::vector>* selections); /// Handle brush mechanics for terrain shape selection - void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode, bool dragOperation); + void selectTerrainShapes (const std::pair& vertexCoords, unsigned char selectMode); /// Push terrain shape edits to command macro void pushEditToCommand (const CSMWorld::LandHeightsColumn::DataType& newLandGrid, CSMDoc::Document& document, diff --git a/apps/opencs/view/render/terrainstorage.cpp b/apps/opencs/view/render/terrainstorage.cpp index d9cc3015e1..035c073567 100644 --- a/apps/opencs/view/render/terrainstorage.cpp +++ b/apps/opencs/view/render/terrainstorage.cpp @@ -48,15 +48,14 @@ namespace CSVRender float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { float height = 0.f; - osg::ref_ptr land = getLand (cellX, cellY); - if (land) - { - const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; - if (data) height = getVertexHeight(data, inCellX, inCellY); - } - else return height; - return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; + int index = mData.getLand().searchId(CSMWorld::Land::createUniqueRecordId(cellX, cellY)); + if (index == -1) // no land! + return height; + + const ESM::Land::LandData* landData = mData.getLand().getRecord(index).get().getLandData(ESM::Land::DATA_VHGT); + height = landData->mHeights[inCellY*ESM::Land::LAND_SIZE + inCellX]; + return mAlteredHeight[inCellY*ESM::Land::LAND_SIZE + inCellX] + height; } float* TerrainStorage::getAlteredHeight(int inCellX, int inCellY) diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index dfcc29ae01..113acd2a21 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -129,7 +129,8 @@ void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResu { if(hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + mTerrainTextureSelection->clearTemporarySelection(); } } @@ -137,7 +138,8 @@ void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitRe { if(hit.hit && hit.tag == nullptr) { - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, false); + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + mTerrainTextureSelection->clearTemporarySelection(); } } @@ -188,8 +190,8 @@ bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos) mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); + return true; } bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) @@ -201,8 +203,8 @@ bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos) mDragMode = InteractionType_None; return false; } - selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); - return false; + selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); + return true; } void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor) @@ -225,13 +227,13 @@ void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diff if (mDragMode == InteractionType_PrimarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0, true); + if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); } if (mDragMode == InteractionType_SecondarySelect) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask()); - if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1, true); + if (hit.hit && hit.tag == nullptr) selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); } } @@ -251,6 +253,8 @@ void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) mIsEditing = false; } } + + mTerrainTextureSelection->clearTemporarySelection(); } void CSVRender::TerrainTextureMode::dragAborted() @@ -496,7 +500,7 @@ bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int } -void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode, bool dragOperation) +void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; @@ -559,8 +563,21 @@ void CSVRender::TerrainTextureMode::selectTerrainTextures(const std::paironlySelect(selections); - if(selectMode == 1) mTerrainTextureSelection->toggleSelect(selections, dragOperation); + std::string selectAction; + + if (selectMode == 0) + selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); + else + selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); + + if (selectAction == "Select only") + mTerrainTextureSelection->onlySelect(selections); + else if (selectAction == "Add to selection") + mTerrainTextureSelection->addSelect(selections); + else if (selectAction == "Remove from selection") + mTerrainTextureSelection->removeSelect(selections); + else if (selectAction == "Invert selection") + mTerrainTextureSelection->toggleSelect(selections); } void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp index e0c6e4b40f..06c95e3d79 100644 --- a/apps/opencs/view/render/terraintexturemode.hpp +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -95,7 +95,7 @@ namespace CSVRender bool isInCellSelection(int globalSelectionX, int globalSelectionY); /// \brief Handle brush mechanics for texture selection - void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode, bool dragOperation); + void selectTerrainTextures (const std::pair& texCoords, unsigned char selectMode); /// \brief Push texture edits to command macro void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index c51479f897..d3bf1fcf0c 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -464,7 +464,6 @@ void CSVRender::WorldspaceWidget::abortDrag() EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.dragAborted(); - mDragging = false; mDragMode = InteractionType_None; } } diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 7334d893db..94babb6ab1 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -19,7 +19,7 @@ set(GAME_HEADER source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor @@ -27,7 +27,7 @@ add_openmw_dir (mwrender add_openmw_dir (mwinput actions actionmanager bindingsmanager controllermanager controlswitch - inputmanagerimp mousemanager keyboardmanager sdlmappings sensormanager + inputmanagerimp mousemanager keyboardmanager sensormanager ) add_openmw_dir (mwgui @@ -72,9 +72,9 @@ add_openmw_dir (mwworld containerstore actiontalk actiontake manualref player cellvisitors failedaction cells localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat - store esmstore recordcmp fallback actionrepair actionsoulgem livecellref actiondoor + store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager - cellpreloader datetimemanager + cellpreloader datetimemanager groundcoverstore magiceffects ) add_openmw_dir (mwphysics @@ -94,7 +94,7 @@ add_openmw_dir (mwmechanics aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning character actors objects aistate trading weaponpriority spellpriority weapontype spellutil - spellabsorption spelleffects + spelleffects ) add_openmw_dir (mwstate @@ -153,9 +153,12 @@ target_link_libraries(openmw "osg-ffmpeg-videoplayer" "oics" components - ${LUA_LIBRARIES} ) +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16) + target_precompile_headers(openmw PRIVATE ${SOL_INCLUDE_DIR}/sol/sol.hpp) +endif () + if (ANDROID) target_link_libraries(openmw EGL android log z) endif (ANDROID) @@ -176,8 +179,7 @@ endif() if(APPLE) set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") - set(OPENMW_MYGUI_FILES_ROOT ${BUNDLE_RESOURCES_DIR}) - set(OPENMW_SHADERS_ROOT ${BUNDLE_RESOURCES_DIR}) + set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) diff --git a/apps/openmw/android_main.cpp b/apps/openmw/android_main.cpp index cc36388b0b..95365915df 100644 --- a/apps/openmw/android_main.cpp +++ b/apps/openmw/android_main.cpp @@ -1,4 +1,6 @@ +#ifndef stderr int stderr = 0; // Hack: fix linker error +#endif #include "SDL_main.h" #include diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index bbe4f25f69..a44a02ef18 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -8,6 +8,8 @@ #include +#include + #include #include #include @@ -37,11 +39,10 @@ #include -#include - #include #include +#include #include "mwinput/inputmanagerimp.hpp" @@ -293,6 +294,10 @@ bool OMW::Engine::frame(float frametime) // Main menu opened? Then scripts are also paused. bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu); + // Should be called after input manager update and before any change to the game world. + // It applies to the game world queued changes from the previous frame. + mLuaManager->synchronizedUpdate(mEnvironment.getWindowManager()->isGuiMode(), frametime); + // update game state { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); @@ -495,11 +500,6 @@ void OMW::Engine::addGroundcoverFile(const std::string& file) mGroundcoverFiles.emplace_back(file); } -void OMW::Engine::addLuaScriptListFile(const std::string& file) -{ - mLuaScriptListFiles.push_back(file); -} - void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) { mSkipMenu = skipMenu; @@ -551,6 +551,10 @@ void OMW::Engine::createWindow(Settings::Manager& settings) if(fullscreen) flags |= SDL_WINDOW_FULLSCREEN; + // Allows for Windows snapping features to properly work in borderless window + SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "1"); + SDL_SetHint("SDL_BORDERLESS_RESIZABLE_STYLE", "1"); + if (!windowBorder) flags |= SDL_WINDOW_BORDERLESS; @@ -674,7 +678,7 @@ void OMW::Engine::setWindowIcon() void OMW::Engine::prepareEngine (Settings::Manager & settings) { mEnvironment.setStateManager ( - new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles)); createWindow(settings); @@ -714,7 +718,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mViewer->addEventHandler(mScreenCaptureHandler); - mLuaManager = new MWLua::LuaManager(mVFS.get(), mLuaScriptListFiles); + mLuaManager = new MWLua::LuaManager(mVFS.get()); mEnvironment.setLuaManager(mLuaManager); // Create input and UI first to set up a bootstrapping environment for @@ -758,6 +762,28 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) osg::ref_ptr exts = osg::GLExtensions::Get(0, false); bool shadersSupported = exts && (exts->glslLanguageVersion >= 1.2f); + bool enableReverseZ = false; + + if (Settings::Manager::getBool("reverse z", "Camera")) + { + if (exts && exts->isClipControlSupported) + { + enableReverseZ = true; + Log(Debug::Info) << "Using reverse-z depth buffer"; + } + else + Log(Debug::Warning) << "GL_ARB_clip_control not supported: disabling reverse-z depth buffer"; + } + else + Log(Debug::Info) << "Using standard depth buffer"; + + SceneUtil::AutoDepth::setReversed(enableReverseZ); + +#if OSG_VERSION_LESS_THAN(3, 6, 6) + // hack fix for https://github.com/openscenegraph/OpenSceneGraph/issues/1028 + if (exts) + exts->glRenderbufferStorageMultisampleCoverageNV = nullptr; +#endif std::string myguiResources = (mResDir / "mygui").string(); osg::ref_ptr guiRoot = new osg::Group; @@ -869,7 +895,6 @@ public: } else update(); - mEngine->mLuaManager->applyQueuedChanges(); }; void join() @@ -1064,8 +1089,6 @@ void OMW::Engine::go() // Save user settings settings.saveUser(settingspath); - mViewer->stopThreading(); - Log(Debug::Info) << "Quitting peacefully."; } diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 290fd890a6..5fe032d27e 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -72,7 +72,6 @@ namespace OMW std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; - std::vector mLuaScriptListFiles; bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -146,7 +145,6 @@ namespace OMW */ void addContentFile(const std::string& file); void addGroundcoverFile(const std::string& file); - void addLuaScriptListFile(const std::string& file); /// Disable or enable all sounds void setSoundUsage(bool soundUsage); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d502ecbc05..d5bed4ba25 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -123,9 +123,11 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat engine.addGroundcoverFile(file); } - StringsVector luaScriptLists = variables["lua-scripts"].as(); - for (const auto& file : luaScriptLists) - engine.addLuaScriptListFile(file); + if (variables.count("lua-scripts")) + { + Log(Debug::Warning) << "Lua scripts have been specified via the old lua-scripts option and will not be loaded. " + "Please update them to a version which uses the new omwscripts format."; + } // startup-settings engine.setCell(variables["start"].as()); diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 5ac7c218b5..d475d93cd1 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -49,8 +49,8 @@ namespace MWBase virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; virtual void setAttemptJump(bool jumping) = 0; - virtual void toggleControlSwitch (const std::string& sw, bool value) = 0; - virtual bool getControlSwitch (const std::string& sw) = 0; + virtual void toggleControlSwitch(std::string_view sw, bool value) = 0; + virtual bool getControlSwitch(std::string_view sw) = 0; virtual std::string getActionDescription (int action) const = 0; virtual std::string getActionKeyBindingName (int action) const = 0; @@ -58,8 +58,8 @@ namespace MWBase virtual bool actionIsActive(int action) const = 0; virtual float getActionValue(int action) const = 0; // returns value in range [0, 1] + virtual bool isControllerButtonPressed(SDL_GameControllerButton button) const = 0; virtual float getControllerAxisValue(SDL_GameControllerAxis axis) const = 0; // returns value in range [-1, 1] - virtual uint32_t getMouseButtonsState() const = 0; virtual int getMouseMoveX() const = 0; virtual int getMouseMoveY() const = 0; diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index ebcd8f50b3..cc479a2937 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -30,6 +30,7 @@ namespace MWBase virtual ~LuaManager() = default; virtual void newGameStarted() = 0; + virtual void gameLoaded() = 0; virtual void registerObject(const MWWorld::Ptr& ptr) = 0; virtual void deregisterObject(const MWWorld::Ptr& ptr) = 0; virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 6bedbb5b4d..484940e3e6 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -112,6 +112,9 @@ namespace MWBase /// Makes \a ptr fight \a target. Also shouts a combat taunt. virtual void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; + /// Removes an actor and its allies from combat with the actor's targets. + virtual void stopCombat(const MWWorld::Ptr& ptr) = 0; + enum OffenseType { OT_Theft, // Taking items owned by an NPC or a faction you are not a member of diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 961a63ac79..958bb466ab 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -355,6 +355,7 @@ namespace MWBase virtual const std::string& getVersionDescription() const = 0; virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0; + virtual void forceLootMode(const MWWorld::Ptr& ptr) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 8b33095fde..d1747a2e39 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -62,6 +62,7 @@ namespace MWPhysics namespace MWRender { class Animation; + class Camera; } namespace MWMechanics @@ -289,7 +290,7 @@ namespace MWBase virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; @@ -433,14 +434,12 @@ namespace MWBase virtual osg::Matrixf getActorHeadTransform(const MWWorld::ConstPtr& actor) const = 0; + virtual MWRender::Camera* getCamera() = 0; virtual void togglePOV(bool force = false) = 0; virtual bool isFirstPerson() const = 0; virtual bool isPreviewModeEnabled() const = 0; - virtual void togglePreviewMode(bool enable) = 0; virtual bool toggleVanityMode(bool enable) = 0; - virtual void allowVanityMode(bool allow) = 0; virtual bool vanityRotateCamera(float * rot) = 0; - virtual void adjustCameraDistance(float dist) = 0; virtual void applyDeferredPreviewRotationToPlayer(float dt) = 0; virtual void disableDeferredPreviewRotation() = 0; diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 6285bdbf7e..6c53ba72f3 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -89,7 +89,7 @@ namespace MWClass { std::shared_ptr instance (new Activator); - registerClass (typeid (ESM::Activator).name(), instance); + registerClass (ESM::Activator::sRecordId, instance); } bool Activator::hasToolTip (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index 886ffe4771..596bdf26ec 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -3,6 +3,8 @@ #include "../mwworld/class.hpp" +#include + namespace ESM { struct GameSetting; @@ -17,6 +19,15 @@ namespace MWClass Actor() = default; + template + float getSwimSpeedImpl(const MWWorld::Ptr& ptr, const GMST& gmst, const MWMechanics::MagicEffects& mageffects, float baseSpeed) const + { + return baseSpeed + * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) + * (gmst.fSwimRunBase->mValue.getFloat() + + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + } + public: ~Actor() override = default; diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index e09e4804ce..6e9d8b3779 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -69,7 +69,7 @@ namespace MWClass { std::shared_ptr instance (new Apparatus); - registerClass (typeid (ESM::Apparatus).name(), instance); + registerClass (ESM::Apparatus::sRecordId, instance); } std::string Apparatus::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 817adbc1f5..bc2d788b5b 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -163,7 +163,7 @@ namespace MWClass { std::shared_ptr instance (new Armor); - registerClass (typeid (ESM::Armor).name(), instance); + registerClass (ESM::Armor::sRecordId, instance); } std::string Armor::getUpSoundId (const MWWorld::ConstPtr& ptr) const @@ -322,7 +322,7 @@ namespace MWClass if(*slot == MWWorld::InventoryStore::Slot_CarriedLeft) { MWWorld::ConstContainerStoreIterator weapon = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != invStore.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + if(weapon != invStore.end() && weapon->getType() == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); if (MWMechanics::getWeaponType(ref->mBase->mData.mType)->mFlags & ESM::WeaponType::TwoHanded) diff --git a/apps/openmw/mwclass/bodypart.cpp b/apps/openmw/mwclass/bodypart.cpp index 7fe798e27d..06b460f75c 100644 --- a/apps/openmw/mwclass/bodypart.cpp +++ b/apps/openmw/mwclass/bodypart.cpp @@ -36,7 +36,7 @@ namespace MWClass { std::shared_ptr instance (new BodyPart); - registerClass (typeid (ESM::BodyPart).name(), instance); + registerClass (ESM::BodyPart::sRecordId, instance); } std::string BodyPart::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 51b9e39d7a..eef8e02808 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -85,7 +85,7 @@ namespace MWClass { std::shared_ptr instance (new Book); - registerClass (typeid (ESM::Book).name(), instance); + registerClass (ESM::Book::sRecordId, instance); } std::string Book::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 400cd97e41..ce8c79d02e 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -121,7 +121,7 @@ namespace MWClass { std::shared_ptr instance (new Clothing); - registerClass (typeid (ESM::Clothing).name(), instance); + registerClass (ESM::Clothing::sRecordId, instance); } std::string Clothing::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 06980f55da..0f45c25744 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -242,7 +242,7 @@ namespace MWClass { std::shared_ptr instance (new Container); - registerClass (typeid (ESM::Container).name(), instance); + registerClass (ESM::Container::sRecordId, instance); } bool Container::hasToolTip (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 54623e6699..0b3ffa924f 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -82,12 +82,13 @@ namespace MWClass const Creature::GMST& Creature::getGmst() { - static GMST gmst; - static bool inited = false; - if (!inited) + static const GMST staticGmst = [] { + GMST gmst; + const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); + gmst.fMinWalkSpeedCreature = store.find("fMinWalkSpeedCreature"); gmst.fMaxWalkSpeedCreature = store.find("fMaxWalkSpeedCreature"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); @@ -101,16 +102,20 @@ namespace MWClass gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); - inited = true; - } - return gmst; + + return gmst; + } (); + return staticGmst; } void Creature::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data (new CreatureCustomData); + auto tempData = std::make_unique(); + CreatureCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -154,10 +159,7 @@ namespace MWClass data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); - data->mCreatureStats.setNeedRecalcDynamicStats(false); - - // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); @@ -238,7 +240,7 @@ namespace MWClass { MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weaponslot != inv.end() && weaponslot->getTypeName() == typeid(ESM::Weapon).name()) + if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) weapon = *weaponslot; } @@ -497,7 +499,7 @@ namespace MWClass { std::shared_ptr instance (new Creature); - registerClass (typeid (ESM::Creature).name(), instance); + registerClass (ESM::Creature::sRecordId, instance); } float Creature::getMaxSpeed(const MWWorld::Ptr &ptr) const @@ -891,12 +893,8 @@ namespace MWClass float Creature::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); - const GMST& gmst = getGmst(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); - return getWalkSpeed(ptr) - * (1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude()) - * (gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat()); + return getSwimSpeedImpl(ptr, getGmst(), mageffects, getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/creaturelevlist.cpp b/apps/openmw/mwclass/creaturelevlist.cpp index f86004c619..ee33242126 100644 --- a/apps/openmw/mwclass/creaturelevlist.cpp +++ b/apps/openmw/mwclass/creaturelevlist.cpp @@ -5,6 +5,7 @@ #include "../mwmechanics/levelledlist.hpp" +#include "../mwworld/cellstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -27,6 +28,24 @@ namespace MWClass } }; + MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const + { + const MWWorld::LiveCellRef *ref = ptr.get(); + + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const + { + if (ptr.getRefData().getCustomData() == nullptr) + return; + + CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); + MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if (!creature.isEmpty()) + MWBase::Environment::get().getWorld()->adjustPosition(creature, force); + } + std::string CreatureLevList::getName (const MWWorld::ConstPtr& ptr) const { return ""; @@ -45,7 +64,13 @@ namespace MWClass if (customData.mSpawn) return; - MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + MWWorld::Ptr creature; + if(customData.mSpawnActorId != -1) + { + creature = MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); + if(creature.isEmpty()) + creature = ptr.getCell()->getMovedActor(customData.mSpawnActorId); + } if (!creature.isEmpty()) { const MWMechanics::CreatureStats& creatureStats = creature.getClass().getCreatureStats(creature); @@ -70,7 +95,7 @@ namespace MWClass { std::shared_ptr instance (new CreatureLevList); - registerClass (typeid (ESM::CreatureLevList).name(), instance); + registerClass (ESM::CreatureLevList::sRecordId, instance); } void CreatureLevList::getModelsToPreload(const MWWorld::Ptr &ptr, std::vector &models) const diff --git a/apps/openmw/mwclass/creaturelevlist.hpp b/apps/openmw/mwclass/creaturelevlist.hpp index 35152a9422..b3a940682c 100644 --- a/apps/openmw/mwclass/creaturelevlist.hpp +++ b/apps/openmw/mwclass/creaturelevlist.hpp @@ -32,6 +32,10 @@ namespace MWClass ///< Write additional state from \a ptr into \a state. void respawn (const MWWorld::Ptr& ptr) const override; + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const override; + + void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index b5fe705ca6..01ff2aa440 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -264,7 +264,7 @@ namespace MWClass { std::shared_ptr instance (new Door); - registerClass (typeid (ESM::Door).name(), instance); + registerClass (ESM::Door::sRecordId, instance); } MWGui::ToolTipInfo Door::getToolTipInfo (const MWWorld::ConstPtr& ptr, int count) const diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index 20f9576dff..f582812934 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -81,7 +81,7 @@ namespace MWClass { std::shared_ptr instance (new Ingredient); - registerClass (typeid (ESM::Ingredient).name(), instance); + registerClass (ESM::Ingredient::sRecordId, instance); } std::string Ingredient::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/itemlevlist.cpp b/apps/openmw/mwclass/itemlevlist.cpp index 5608a8d233..4ca45152a1 100644 --- a/apps/openmw/mwclass/itemlevlist.cpp +++ b/apps/openmw/mwclass/itemlevlist.cpp @@ -19,6 +19,6 @@ namespace MWClass { std::shared_ptr instance (new ItemLevList); - registerClass (typeid (ESM::ItemLevList).name(), instance); + registerClass (ESM::ItemLevList::sRecordId, instance); } } diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index 69cc1a09bf..dbd4e8a184 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -124,7 +124,7 @@ namespace MWClass { std::shared_ptr instance (new Light); - registerClass (typeid (ESM::Light).name(), instance); + registerClass (ESM::Light::sRecordId, instance); } std::string Light::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 985b087711..ccb5bbbd58 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -80,7 +80,7 @@ namespace MWClass { std::shared_ptr instance (new Lockpick); - registerClass (typeid (ESM::Lockpick).name(), instance); + registerClass (ESM::Lockpick::sRecordId, instance); } std::string Lockpick::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index facab9d51c..30e68d2377 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -106,7 +106,7 @@ namespace MWClass { std::shared_ptr instance (new Miscellaneous); - registerClass (typeid (ESM::Miscellaneous).name(), instance); + registerClass (ESM::Miscellaneous::sRecordId, instance); } std::string Miscellaneous::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 718b4d972d..5d50ba558f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -266,10 +266,10 @@ namespace MWClass const Npc::GMST& Npc::getGmst() { - static GMST gmst; - static bool inited = false; - if(!inited) + static const GMST staticGmst = [] { + GMST gmst; + const MWBase::World *world = MWBase::Environment::get().getWorld(); const MWWorld::Store &store = world->getStore().get(); @@ -294,16 +294,20 @@ namespace MWClass gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); - inited = true; - } - return gmst; + return gmst; + } (); + return staticGmst; } void Npc::ensureCustomData (const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { - std::unique_ptr data(new NpcCustomData); + bool recalculate = false; + auto tempData = std::make_unique(); + NpcCustomData* data = tempData.get(); + MWMechanics::CreatureCustomDataResetter resetter(ptr); + ptr.getRefData().setCustomData(std::move(tempData)); MWWorld::LiveCellRef *ref = ptr.get(); @@ -334,8 +338,6 @@ namespace MWClass data->mNpcStats.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); - - data->mNpcStats.setNeedRecalcDynamicStats(false); } else { @@ -351,7 +353,7 @@ namespace MWClass autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); - data->mNpcStats.setNeedRecalcDynamicStats(true); + recalculate = true; } // Persistent actors with 0 health do not play death animation @@ -387,7 +389,9 @@ namespace MWClass data->mNpcStats.setGoldPool(gold); // store - ptr.getRefData().setCustomData(std::move(data)); + resetter.mPtr = {}; + if(recalculate) + data->mNpcStats.recalculateMagicka(); // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items @@ -459,12 +463,12 @@ namespace MWClass if (equipped != invStore.end()) { std::vector parts; - if(equipped->getTypeName() == typeid(ESM::Clothing).name()) + if(equipped->getType() == ESM::Clothing::sRecordId) { const ESM::Clothing *clothes = equipped->get()->mBase; parts = clothes->mParts.mParts; } - else if(equipped->getTypeName() == typeid(ESM::Armor).name()) + else if(equipped->getType() == ESM::Armor::sRecordId) { const ESM::Armor *armor = equipped->get()->mBase; parts = armor->mParts.mParts; @@ -543,7 +547,7 @@ namespace MWClass MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr weapon = ((weaponslot != inv.end()) ? *weaponslot : MWWorld::Ptr()); - if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) + if(!weapon.isEmpty() && weapon.getType() != ESM::Weapon::sRecordId) weapon = MWWorld::Ptr(); MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); @@ -766,7 +770,7 @@ namespace MWClass MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); - bool hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; // If there's no item in the carried left slot or if it is not a shield redistribute the hit. if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) { @@ -778,7 +782,7 @@ namespace MWClass if (armorslot != inv.end()) { armor = *armorslot; - hasArmor = !armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name(); + hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; } } if (hasArmor) @@ -1036,7 +1040,7 @@ namespace MWClass void Npc::registerSelf() { std::shared_ptr instance (new Npc); - registerClass (typeid (ESM::NPC).name(), instance); + registerClass (ESM::NPC::sRecordId, instance); } bool Npc::hasToolTip(const MWWorld::ConstPtr& ptr) const @@ -1135,7 +1139,7 @@ namespace MWClass for(int i = 0;i < MWWorld::InventoryStore::Slots;i++) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(i); - if (it == invStore.end() || it->getTypeName() != typeid(ESM::Armor).name()) + if (it == invStore.end() || it->getType() != ESM::Armor::sRecordId) { // unarmored ratings[i] = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); @@ -1232,7 +1236,7 @@ namespace MWClass const MWWorld::InventoryStore &inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); - if(boots == inv.end() || boots->getTypeName() != typeid(ESM::Armor).name()) + if(boots == inv.end() || boots->getType() != ESM::Armor::sRecordId) return (name == "left") ? "FootBareLeft" : "FootBareRight"; switch(boots->getClass().getEquipmentSkill(*boots)) @@ -1476,7 +1480,6 @@ namespace MWClass float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { - const GMST& gmst = getGmst(); const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); const NpcCustomData* npcdata = static_cast(ptr.getRefData().getCustomData()); @@ -1486,17 +1489,6 @@ namespace MWClass const bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run) && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); - float swimSpeed; - - if (running) - swimSpeed = getRunSpeed(ptr); - else - swimSpeed = getWalkSpeed(ptr); - - swimSpeed *= 1.0f + 0.01f * mageffects.get(ESM::MagicEffect::SwiftSwim).getMagnitude(); - swimSpeed *= gmst.fSwimRunBase->mValue.getFloat() - + 0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fSwimRunAthleticsMult->mValue.getFloat(); - - return swimSpeed; + return getSwimSpeedImpl(ptr, getGmst(), mageffects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 56d9dff279..e0f8cf8397 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -74,7 +74,7 @@ namespace MWClass { std::shared_ptr instance (new Potion); - registerClass (typeid (ESM::Potion).name(), instance); + registerClass (ESM::Potion::sRecordId, instance); } std::string Potion::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 51273337a6..8291fb8f3c 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -80,7 +80,7 @@ namespace MWClass { std::shared_ptr instance (new Probe); - registerClass (typeid (ESM::Probe).name(), instance); + registerClass (ESM::Probe::sRecordId, instance); } std::string Probe::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index f1b88e422b..42581a8b6b 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -69,7 +69,7 @@ namespace MWClass { std::shared_ptr instance (new Repair); - registerClass (typeid (ESM::Repair).name(), instance); + registerClass (ESM::Repair::sRecordId, instance); } std::string Repair::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 0805ca3dd1..fc350c8351 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -59,7 +59,7 @@ namespace MWClass { std::shared_ptr instance (new Static); - registerClass (typeid (ESM::Static).name(), instance); + registerClass (ESM::Static::sRecordId, instance); } MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 6246c8fb09..e7337c83b7 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -125,7 +125,7 @@ namespace MWClass { std::shared_ptr instance (new Weapon); - registerClass (typeid (ESM::Weapon).name(), instance); + registerClass (ESM::Weapon::sRecordId, instance); } std::string Weapon::getUpSoundId (const MWWorld::ConstPtr& ptr) const diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 8aa6acd8ed..9800f1b39c 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -76,9 +76,10 @@ namespace MWDialogue mKnownTopics.insert( Misc::StringUtils::lowerCase(topic) ); } - void DialogueManager::parseText (const std::string& text) + std::vector DialogueManager::parseTopicIdsFromText (const std::string& text) { - updateActorKnownTopics(); + std::vector topicIdList; + std::vector hypertext = HyperTextParser::parseHyperText(text); for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) @@ -95,6 +96,18 @@ namespace MWDialogue topicId = mTranslationDataStorage.topicStandardForm(topicId); } + topicIdList.push_back(topicId); + } + + return topicIdList; + } + + void DialogueManager::addTopicsFromText (const std::string& text) + { + updateActorKnownTopics(); + + for (const auto& topicId : parseTopicIdsFromText(text)) + { if (mActorKnownTopics.count( topicId )) mKnownTopics.insert( topicId ); } @@ -136,7 +149,6 @@ namespace MWDialogue mTalkedTo = creatureStats.hasTalkedToPlayer(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); //greeting const MWWorld::Store &dialogs = @@ -163,7 +175,7 @@ namespace MWDialogue executeScript (info->mResultScript, mActor); mLastTopic = it->mId; - parseText (info->mResponse); + addTopicsFromText (info->mResponse); return true; } @@ -277,7 +289,10 @@ namespace MWDialogue const ESM::Dialogue& dialogue = *dialogues.find (topic); - const ESM::DialInfo* info = filter.search(dialogue, true); + const ESM::DialInfo* info = + mChoice == -1 && mActorKnownTopics.count(topic) ? + mActorKnownTopics[topic].mInfo : filter.search(dialogue, true); + if (info) { std::string title; @@ -320,7 +335,7 @@ namespace MWDialogue executeScript (info->mResultScript, mActor); - parseText (info->mResponse); + addTopicsFromText (info->mResponse); } } @@ -339,7 +354,6 @@ namespace MWDialogue updateGlobals(); mActorKnownTopics.clear(); - mActorKnownTopicsFlag.clear(); const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get(); @@ -354,21 +368,41 @@ namespace MWDialogue if (answer != nullptr) { - int flag = 0; + int topicFlags = 0; if(!inJournal(topicId, answer->mId)) { // Does this dialogue contains some actor-specific answer? if (Misc::StringUtils::ciEqual(answer->mActor, mActor.getCellRef().getRefId())) - flag |= MWBase::DialogueManager::TopicType::Specific; + topicFlags |= MWBase::DialogueManager::TopicType::Specific; } else - flag |= MWBase::DialogueManager::TopicType::Exhausted; - mActorKnownTopics.insert (dialog.mId); - mActorKnownTopicsFlag[dialog.mId] = flag; + topicFlags |= MWBase::DialogueManager::TopicType::Exhausted; + mActorKnownTopics.insert (std::make_pair(dialog.mId, ActorKnownTopicInfo {topicFlags, answer})); } } } + + // If response to a topic leads to a new topic, the original topic is not exhausted. + + for (auto& [dialogId, topicInfo] : mActorKnownTopics) + { + // If the topic is not marked as exhausted, we don't need to do anything about it. + // If the topic will not be shown to the player, the flag actually does not matter. + + if (!(topicInfo.mFlags & MWBase::DialogueManager::TopicType::Exhausted) || + !mKnownTopics.count(dialogId)) + continue; + + for (const auto& topicId : parseTopicIdsFromText(topicInfo.mInfo->mResponse)) + { + if (mActorKnownTopics.count( topicId ) && !mKnownTopics.count( topicId )) + { + topicInfo.mFlags &= ~MWBase::DialogueManager::TopicType::Exhausted; + break; + } + } + } } std::list DialogueManager::getAvailableTopics() @@ -377,7 +411,7 @@ namespace MWDialogue std::list keywordList; - for (const std::string& topic : mActorKnownTopics) + for (const auto& [topic, topicInfo] : mActorKnownTopics) { //does the player know the topic? if (mKnownTopics.count(topic)) @@ -391,7 +425,7 @@ namespace MWDialogue int DialogueManager::getTopicFlag(const std::string& topicId) { - return mActorKnownTopicsFlag[topicId]; + return mActorKnownTopics[topicId].mFlags; } void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback) @@ -421,7 +455,7 @@ namespace MWDialogue // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) npcStats.setBaseDisposition(0); int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); - int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero)); + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); npcStats.setBaseDisposition(disposition); } @@ -444,7 +478,7 @@ namespace MWDialogue if (const ESM::DialInfo *info = filter.search (*dialogue, true)) { std::string text = info->mResponse; - parseText (text); + addTopicsFromText (text); mChoice = -1; mIsInChoice = false; @@ -579,7 +613,7 @@ namespace MWDialogue { const ESM::DialInfo* info = infos[0]; - parseText (info->mResponse); + addTopicsFromText (info->mResponse); const MWWorld::Store& gmsts = MWBase::Environment::get().getWorld()->getStore().get(); diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index ab2625ff5a..57eb74d0a6 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwworld/ptr.hpp" @@ -24,14 +25,19 @@ namespace MWDialogue { class DialogueManager : public MWBase::DialogueManager { + struct ActorKnownTopicInfo + { + int mFlags; + const ESM::DialInfo* mInfo; + }; + std::set mKnownTopics;// Those are the topics the player knows. // Modified faction reactions. > typedef std::map > ModFactionReactionMap; ModFactionReactionMap mChangedFactionReaction; - std::set mActorKnownTopics; - std::unordered_map mActorKnownTopicsFlag; + std::map mActorKnownTopics; Translation::Storage& mTranslationDataStorage; MWScript::CompilerContext mCompilerContext; @@ -51,7 +57,8 @@ namespace MWDialogue int mCurrentDisposition; int mPermanentDispositionChange; - void parseText (const std::string& text); + std::vector parseTopicIdsFromText (const std::string& text); + void addTopicsFromText (const std::string& text); void updateActorKnownTopics(); void updateGlobals(); diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index 334a9db39f..a47334a2dd 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -23,7 +23,7 @@ bool MWDialogue::Filter::testActor (const ESM::DialInfo& info) const { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); // actor id if (!info.mActor.empty()) @@ -160,7 +160,7 @@ bool MWDialogue::Filter::testSelectStructs (const ESM::DialInfo& info) const bool MWDialogue::Filter::testDisposition (const ESM::DialInfo& info, bool invert) const { - bool isCreature = (mActor.getTypeName() != typeid (ESM::NPC).name()); + bool isCreature = (mActor.getType() != ESM::NPC::sRecordId); if (isCreature) return true; @@ -207,7 +207,7 @@ bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& sele bool MWDialogue::Filter::testSelectStruct (const SelectWrapper& select) const { - if (select.isNpcOnly() && (mActor.getTypeName() != typeid (ESM::NPC).name())) + if (select.isNpcOnly() && (mActor.getType() != ESM::NPC::sRecordId)) // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; @@ -452,7 +452,7 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con { if (target.getClass().isNpc() && target.getClass().getNpcStats(target).isWerewolf()) return 2; - if (target.getTypeName() == typeid(ESM::Creature).name()) + if (target.getType() == ESM::Creature::sRecordId) return 1; } } diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index 0c14ea8f8d..39599457ef 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -30,7 +30,7 @@ public: { if (keyword.empty()) return; - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), 0, mRoot); + seed_impl (std::move(keyword), std::move (value), 0, mRoot); } void clear () @@ -39,7 +39,7 @@ public: mRoot.mKeyword.clear (); } - bool containsKeyword (string_t keyword, value_t& value) + bool containsKeyword (const string_t& keyword, value_t& value) { typename Entry::childen_t::iterator current; typename Entry::childen_t::iterator next; @@ -209,8 +209,8 @@ private: if (j == entry.mChildren.end ()) { - entry.mChildren [ch].mValue = /*std::move*/ (value); - entry.mChildren [ch].mKeyword = /*std::move*/ (keyword); + entry.mChildren [ch].mValue = std::move (value); + entry.mChildren [ch].mKeyword = std::move (keyword); } else { @@ -219,22 +219,22 @@ private: if (keyword == j->second.mKeyword) throw std::runtime_error ("duplicate keyword inserted"); - value_t pushValue = /*std::move*/ (j->second.mValue); - string_t pushKeyword = /*std::move*/ (j->second.mKeyword); + value_t pushValue = j->second.mValue; + string_t pushKeyword = j->second.mKeyword; if (depth >= pushKeyword.size ()) throw std::runtime_error ("unexpected"); if (depth+1 < pushKeyword.size()) { - seed_impl (/*std::move*/ (pushKeyword), /*std::move*/ (pushValue), depth+1, j->second); + seed_impl (std::move (pushKeyword), std::move (pushValue), depth+1, j->second); j->second.mKeyword.clear (); } } if (depth+1 == keyword.size()) j->second.mKeyword = value; else // depth+1 < keyword.size() - seed_impl (/*std::move*/ (keyword), /*std::move*/ (value), depth+1, j->second); + seed_impl (std::move (keyword), std::move (value), depth+1, j->second); } } diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index bacd1c7695..fd4a21cf10 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -194,7 +194,7 @@ namespace MWGui for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; - if (item.getTypeName() != typeid(ESM::Ingredient).name()) + if (item.getType() != ESM::Ingredient::sRecordId) continue; itemNames.insert(item.getClass().getName(item)); diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 874d1d866b..b2d2c39fd6 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -8,7 +8,7 @@ #include "MyGUI_FactoryManager.h" #include -#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -1221,7 +1221,7 @@ public: RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); - float z = SceneUtil::getReverseZ() ? 1.f : -1.f; + float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f; GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), z /*mNode->getNodeDepth()*/, vertices, renderXform); diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index de771051ef..fdfd192cc1 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -38,6 +38,7 @@ namespace MWGui , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) + , mTreatNextOpenAsLoot(false) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); getWidget(mTakeButton, "TakeButton"); @@ -121,13 +122,15 @@ namespace MWGui void ContainerWindow::setPtr(const MWWorld::Ptr& container) { + bool lootAnyway = mTreatNextOpenAsLoot; + mTreatNextOpenAsLoot = false; mPtr = container; bool loot = mPtr.getClass().isActor() && mPtr.getClass().getCreatureStats(mPtr).isDead(); if (mPtr.getClass().hasInventoryStore(mPtr)) { - if (mPtr.getClass().isNpc() && !loot) + if (mPtr.getClass().isNpc() && !loot && !lootAnyway) { // we are stealing stuff mModel = new PickpocketItemModel(mPtr, new InventoryItemModel(container), diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 2a0dee44e2..66a20e7ef5 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -37,6 +37,7 @@ namespace MWGui void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; }; private: DragAndDrop* mDragAndDrop; @@ -44,7 +45,7 @@ namespace MWGui SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; - + bool mTreatNextOpenAsLoot; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; MyGUI::Button* mCloseButton; diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index 81dde1c059..9f202108a2 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -209,7 +209,7 @@ bool ContainerItemModel::onDropItem(const MWWorld::Ptr &item, int count) MWWorld::Ptr target = mItemSources[0].first; - if (target.getTypeName() != typeid(ESM::Container).name()) + if (target.getType() != ESM::Container::sRecordId) return true; // check container organic flag diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 93180adad2..86cad0fa7a 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -347,8 +347,7 @@ namespace MWGui { if (!mScrollBar->getVisible()) return; - mScrollBar->setScrollPosition(std::min(static_cast(mScrollBar->getScrollRange()-1), - std::max(0, static_cast(mScrollBar->getScrollPosition() - _rel*0.3)))); + mScrollBar->setScrollPosition(std::clamp(mScrollBar->getScrollPosition() - _rel*0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } @@ -508,13 +507,13 @@ namespace MWGui int services = mPtr.getClass().getServices(mPtr); - bool travel = (mPtr.getTypeName() == typeid(ESM::NPC).name() && !mPtr.get()->mBase->getTransport().empty()) - || (mPtr.getTypeName() == typeid(ESM::Creature).name() && !mPtr.get()->mBase->getTransport().empty()); + bool travel = (mPtr.getType() == ESM::NPC::sRecordId && !mPtr.get()->mBase->getTransport().empty()) + || (mPtr.getType() == ESM::Creature::sRecordId && !mPtr.get()->mBase->getTransport().empty()); const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - if (mPtr.getTypeName() == typeid(ESM::NPC).name()) + if (mPtr.getType() == ESM::NPC::sRecordId) mTopicsList->addItem(gmst.find("sPersuasion")->mValue.getString()); if (services & ESM::NPC::AllItems) diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index d0d2118c6e..ee0067f082 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -109,7 +109,7 @@ namespace MWGui { mEnchantmentPoints->setCaption(std::to_string(static_cast(mEnchanting.getEnchantPoints(false))) + " / " + std::to_string(mEnchanting.getMaxEnchantValue())); mCharge->setCaption(std::to_string(mEnchanting.getGemCharge())); - mSuccessChance->setCaption(std::to_string(std::max(0, std::min(100, mEnchanting.getEnchantChance())))); + mSuccessChance->setCaption(std::to_string(std::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 5a7b5a9590..f6136791e9 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -81,7 +81,7 @@ namespace MWGui , mMinimap(nullptr) , mCrosshair(nullptr) , mCellNameBox(nullptr) - , mDrowningFrame(nullptr) + , mDrowningBar(nullptr) , mDrowningFlash(nullptr) , mHealthManaStaminaBaseLeft(0) , mWeapBoxBaseLeft(0) @@ -119,6 +119,7 @@ namespace MWGui fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); //Drowning bar + getWidget(mDrowningBar, "DrowningBar"); getWidget(mDrowningFrame, "DrowningFrame"); getWidget(mDrowning, "Drowning"); getWidget(mDrowningFlash, "Flash"); @@ -224,7 +225,7 @@ namespace MWGui void HUD::setDrowningBarVisible(bool visible) { - mDrowningFrame->setVisible(visible); + mDrowningBar->setVisible(visible); } void HUD::onWorldClicked(MyGUI::Widget* _sender) @@ -368,9 +369,6 @@ namespace MWGui mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0,20)); } - if (mIsDrowning) - mDrowningFlashTheta += dt * osg::PI*2; - mSpellIcons->updateWidgets(mEffectBox, true); if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) @@ -378,8 +376,13 @@ namespace MWGui updateEnemyHealthBar(); } + if (mDrowningBar->getVisible()) + mDrowningBar->setPosition(mMainWidget->getWidth()/2 - mDrowningFrame->getWidth()/2, mMainWidget->getTop()); + if (mIsDrowning) { + mDrowningFlashTheta += dt * osg::PI*2; + float intensity = (cos(mDrowningFlashTheta) + 2.0f) / 3.0f; mDrowningFlash->setAlpha(intensity); @@ -610,7 +613,7 @@ namespace MWGui static const float fNPCHealthBarFade = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCHealthBarFade")->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) - mEnemyHealth->setAlpha(std::max(0.f, std::min(1.f, mEnemyHealthTimer/fNPCHealthBarFade))); + mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 8a89320d8c..ef591bec97 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -73,7 +73,7 @@ namespace MWGui MyGUI::ImageBox* mCrosshair; MyGUI::TextBox* mCellNameBox; MyGUI::TextBox* mWeaponSpellBox; - MyGUI::Widget *mDrowningFrame, *mDrowningFlash; + MyGUI::Widget *mDrowningBar, *mDrowningFrame, *mDrowningFlash; // bottom left elements int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index f53ba21f9f..0e76c2429f 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -46,7 +46,7 @@ namespace bool isRightHandWeapon(const MWWorld::Ptr& item) { - if (item.getClass().getTypeName() != typeid(ESM::Weapon).name()) + if (item.getClass().getType() != ESM::Weapon::sRecordId) return false; std::vector equipmentSlots = item.getClass().getEquipmentSlots(item).first; return (!equipmentSlots.empty() && equipmentSlots.front() == MWWorld::InventoryStore::Slot_CarriedRight); @@ -70,7 +70,7 @@ namespace MWGui , mTrading(false) , mUpdateTimer(0.f) { - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); @@ -281,7 +281,7 @@ namespace MWGui // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { - bool isWeapon = item.mBase.getTypeName() == typeid(ESM::Weapon).name(); + bool isWeapon = item.mBase.getType() == ESM::Weapon::sRecordId; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if (isWeapon && invStore.isEquipped(item.mBase)) @@ -555,9 +555,9 @@ namespace MWGui if (!script.empty()) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here - const std::string& type = ptr.getTypeName(); - bool isBook = type == typeid(ESM::Book).name(); - if (!isBook && type != typeid(ESM::Ingredient).name() && type != typeid(ESM::Repair).name()) + auto type = ptr.getType(); + bool isBook = type == ESM::Book::sRecordId; + if (!isBook && type != ESM::Ingredient::sRecordId && type != ESM::Repair::sRecordId) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); // Books must have PCSkipEquip set to 1 instead else if (isBook) @@ -593,8 +593,8 @@ namespace MWGui useItem(ptr); // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 item - if ((ptr.getTypeName() == typeid(ESM::Potion).name() || - ptr.getTypeName() == typeid(ESM::Ingredient).name()) + if ((ptr.getType() == ESM::Potion::sRecordId || + ptr.getType() == ESM::Ingredient::sRecordId) && mDragAndDrop->mDraggedCount > 1) { // Item can be provided from other window for example container. @@ -704,19 +704,19 @@ namespace MWGui if (!MWBase::Environment::get().getWindowManager()->isAllowed(GW_Inventory)) return; // make sure the object is of a type that can be picked up - const std::string& type = object.getTypeName(); - if ( (type != typeid(ESM::Apparatus).name()) - && (type != typeid(ESM::Armor).name()) - && (type != typeid(ESM::Book).name()) - && (type != typeid(ESM::Clothing).name()) - && (type != typeid(ESM::Ingredient).name()) - && (type != typeid(ESM::Light).name()) - && (type != typeid(ESM::Miscellaneous).name()) - && (type != typeid(ESM::Lockpick).name()) - && (type != typeid(ESM::Probe).name()) - && (type != typeid(ESM::Repair).name()) - && (type != typeid(ESM::Weapon).name()) - && (type != typeid(ESM::Potion).name())) + auto type = object.getType(); + if ( (type != ESM::Apparatus::sRecordId) + && (type != ESM::Armor::sRecordId) + && (type != ESM::Book::sRecordId) + && (type != ESM::Clothing::sRecordId) + && (type != ESM::Ingredient::sRecordId) + && (type != ESM::Light::sRecordId) + && (type != ESM::Miscellaneous::sRecordId) + && (type != ESM::Lockpick::sRecordId) + && (type != ESM::Probe::sRecordId) + && (type != ESM::Repair::sRecordId) + && (type != ESM::Weapon::sRecordId) + && (type != ESM::Potion::sRecordId)) return; // An object that can be picked up must have a tooltip. @@ -809,7 +809,7 @@ namespace MWGui lastId = item.getCellRef().getRefId(); - if (item.getClass().getTypeName() == typeid(ESM::Weapon).name() && + if (item.getClass().getType() == ESM::Weapon::sRecordId && isRightHandWeapon(item) && item.getClass().canBeEquipped(item, player).first) { diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp index 6b38cd0d9d..fc3fcc3efe 100644 --- a/apps/openmw/mwgui/journalviewmodel.cpp +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -313,9 +313,9 @@ struct JournalViewModelImpl : JournalViewModel for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i) { Utf8Stream stream (i->first.c_str()); - Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek()); + Utf8Stream::UnicodeChar first = Utf8Stream::toLowerUtf8(stream.peek()); - if (first != Misc::StringUtils::toLowerUtf8(character)) + if (first != Utf8Stream::toLowerUtf8(character)) continue; visitor (i->second.getName()); diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index b718b712c0..b4437873c3 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -79,7 +79,7 @@ void KeyboardNavigation::restoreFocus(int mode) if (found != mKeyFocus.end()) { MyGUI::Widget* w = found->second; - if (w && w->getVisible() && w->getEnabled()) + if (w && w->getVisible() && w->getEnabled() && w->getInheritedVisible() && w->getInheritedEnabled()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(found->second); } } @@ -273,7 +273,7 @@ bool KeyboardNavigation::switchFocus(int direction, bool wrap) if (wrap) index = (index + keyFocusList.size())%keyFocusList.size(); else - index = std::min(std::max(0, index), static_cast(keyFocusList.size())-1); + index = std::clamp(index, 0, keyFocusList.size() - 1); MyGUI::Widget* next = keyFocusList[index]; int vertdiff = next->getTop() - focus->getTop(); diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index d3cd626475..61f48d5279 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "windowpinnablebase.hpp" #include diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index a6bfac2a45..e55b9b4878 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -79,44 +79,48 @@ namespace MWGui delete mMagicSelectionDialog; } - void QuickKeysMenu::onOpen() + inline void QuickKeysMenu::validate(int index) { - WindowBase::onOpen(); - MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - - // Check if quick keys are still valid - for (int i=0; i<10; ++i) + switch (mKey[index].type) { - switch (mKey[i].type) + case Type_Unassigned: + case Type_HandToHand: + case Type_Magic: + break; + case Type_Item: + case Type_MagicItem: { - case Type_Unassigned: - case Type_HandToHand: - case Type_Magic: - break; - case Type_Item: - case Type_MagicItem: - { - MWWorld::Ptr item = *mKey[i].button->getUserData(); - // Make sure the item is available and is not broken - if (!item || item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && + MWWorld::Ptr item = *mKey[index].button->getUserData(); + // Make sure the item is available and is not broken + if (!item || item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) - { - // Try searching for a compatible replacement - item = store.findReplacement(mKey[i].id); + { + // Try searching for a compatible replacement + item = store.findReplacement(mKey[index].id); - if (item) - mKey[i].button->setUserData(MWWorld::Ptr(item)); + if (item) + mKey[index].button->setUserData(MWWorld::Ptr(item)); - break; - } + break; } } } } + void QuickKeysMenu::onOpen() + { + WindowBase::onOpen(); + + // Quick key index + for (int index = 0; index < 10; ++index) + { + validate(index); + } + } + void QuickKeysMenu::unassign(keyData* key) { key->button->clearUserStrings(); @@ -329,11 +333,13 @@ namespace MWGui assert(index >= 1 && index <= 10); keyData *key = &mKey[index-1]; - + MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player); + validate(index-1); + // Delay action executing, // if player is busy for now (casting a spell, attacking someone, etc.) bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) @@ -387,9 +393,9 @@ namespace MWGui if (key->type == Type_Item) { - bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); - bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || - item.getTypeName() == typeid(ESM::Lockpick).name(); + bool isWeapon = item.getType() == ESM::Weapon::sRecordId; + bool isTool = item.getType() == ESM::Probe::sRecordId || + item.getType() == ESM::Lockpick::sRecordId; // delay weapon switching if player is busy if (isDelayNeeded && (isWeapon || isTool)) diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index b2742df796..4761c98ceb 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -76,7 +76,8 @@ namespace MWGui void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - + // Check if quick key is still valid + inline void validate(int index); void unassign(keyData* key); }; diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 457594697d..d30eb65eb3 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -138,7 +138,7 @@ namespace MWGui mPreview->rebuild(); mPreview->setAngle (mCurrentAngle); - mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture())); + mPreviewTexture.reset(new osgMyGUI::OSGTexture(mPreview->getTexture(), mPreview->getTextureStateSet())); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 3b4afc852f..69d09f7260 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -171,7 +171,7 @@ namespace MWGui else valueStr = MyGUI::utility::toString(int(value)); - value = std::max(min, std::min(value, max)); + value = std::clamp(value, min, max); value = (value-min)/(max-min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); @@ -232,6 +232,7 @@ namespace MWGui getWidget(mControllerSwitch, "ControllerButton"); getWidget(mWaterTextureSize, "WaterTextureSize"); getWidget(mWaterReflectionDetail, "WaterReflectionDetail"); + getWidget(mWaterRainRippleDetail, "WaterRainRippleDetail"); getWidget(mLightingMethodButton, "LightingMethodButton"); getWidget(mLightsResetButton, "LightsResetButton"); getWidget(mMaxLights, "MaxLights"); @@ -259,6 +260,7 @@ namespace MWGui mWaterTextureSize->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterTextureSizeChanged); mWaterReflectionDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterReflectionDetailChanged); + mWaterRainRippleDetail->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onWaterRainRippleDetailChanged); mLightingMethodButton->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onLightingMethodButtonChanged); mLightsResetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onLightsResetButtonClicked); @@ -267,6 +269,8 @@ namespace MWGui mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + computeMinimumWindowSize(); + center(); mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); @@ -305,10 +309,12 @@ namespace MWGui if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); - int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail)); + int waterReflectionDetail = std::clamp(Settings::Manager::getInt("reflection detail", "Water"), 0, 5); mWaterReflectionDetail->setIndexSelected(waterReflectionDetail); + int waterRainRippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2); + mWaterRainRippleDetail->setIndexSelected(waterRainRippleDetail); + updateMaxLightsComboBox(mMaxLights); mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video")); @@ -392,11 +398,18 @@ namespace MWGui void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { - unsigned int level = std::min((unsigned int)5, (unsigned int)pos); + unsigned int level = static_cast(std::min(pos, 5)); Settings::Manager::setInt("reflection detail", "Water", level); apply(); } + void SettingsWindow::onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos) + { + unsigned int level = static_cast(std::min(pos, 2)); + Settings::Manager::setInt("rain ripple detail", "Water", level); + apply(); + } + void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) @@ -739,6 +752,32 @@ namespace MWGui layoutControlsBox(); } + void SettingsWindow::computeMinimumWindowSize() + { + auto* window = mMainWidget->castType(); + auto minSize = window->getMinSize(); + + // Window should be at minimum wide enough to show all tabs. + int tabBarWidth = 0; + for (uint32_t i = 0; i < mSettingsTab->getItemCount(); i++) + { + tabBarWidth += mSettingsTab->getButtonWidthAt(i); + } + + // Need to include window margins + int margins = mMainWidget->getWidth() - mSettingsTab->getWidth(); + int minimumWindowWidth = tabBarWidth + margins; + + if (minimumWindowWidth > minSize.width) + { + minSize.width = minimumWindowWidth; + window->setMinSize(minSize); + + // Make a dummy call to setSize so MyGUI can apply any resize resulting from the change in MinSize + mMainWidget->setSize(mMainWidget->getSize()); + } + } + void SettingsWindow::resetScrollbars() { mResolutionList->setScrollPosition(0); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 9c28733f9a..d58e94e84a 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -31,6 +31,7 @@ namespace MWGui MyGUI::ComboBox* mWaterTextureSize; MyGUI::ComboBox* mWaterReflectionDetail; + MyGUI::ComboBox* mWaterRainRippleDetail; MyGUI::ComboBox* mMaxLights; MyGUI::ComboBox* mLightingMethodButton; @@ -55,6 +56,7 @@ namespace MWGui void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); + void onWaterRainRippleDetailChanged(MyGUI::ComboBox* _sender, size_t pos); void onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos); void onLightsResetButtonClicked(MyGUI::Widget* _sender); @@ -75,6 +77,8 @@ namespace MWGui void updateSliderLabel(MyGUI::ScrollBar* scroller, const std::string& value); void layoutControlsBox(); + + void computeMinimumWindowSize(); private: void resetScrollbars(); diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index 28b13cdf0d..9d6ed49d3d 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -1,6 +1,7 @@ #include "sortfilteritemmodel.hpp" #include +#include #include #include #include @@ -27,22 +28,22 @@ namespace { - bool compareType(const std::string& type1, const std::string& type2) + bool compareType(unsigned int type1, unsigned int type2) { // this defines the sorting order of types. types that are first in the vector appear before other types. - std::vector mapping; - mapping.emplace_back(typeid(ESM::Weapon).name() ); - mapping.emplace_back(typeid(ESM::Armor).name() ); - mapping.emplace_back(typeid(ESM::Clothing).name() ); - mapping.emplace_back(typeid(ESM::Potion).name() ); - mapping.emplace_back(typeid(ESM::Ingredient).name() ); - mapping.emplace_back(typeid(ESM::Apparatus).name() ); - mapping.emplace_back(typeid(ESM::Book).name() ); - mapping.emplace_back(typeid(ESM::Light).name() ); - mapping.emplace_back(typeid(ESM::Miscellaneous).name() ); - mapping.emplace_back(typeid(ESM::Lockpick).name() ); - mapping.emplace_back(typeid(ESM::Repair).name() ); - mapping.emplace_back(typeid(ESM::Probe).name() ); + std::vector mapping; + mapping.emplace_back(ESM::Weapon::sRecordId ); + mapping.emplace_back(ESM::Armor::sRecordId ); + mapping.emplace_back(ESM::Clothing::sRecordId ); + mapping.emplace_back(ESM::Potion::sRecordId ); + mapping.emplace_back(ESM::Ingredient::sRecordId ); + mapping.emplace_back(ESM::Apparatus::sRecordId ); + mapping.emplace_back(ESM::Book::sRecordId ); + mapping.emplace_back(ESM::Light::sRecordId ); + mapping.emplace_back(ESM::Miscellaneous::sRecordId ); + mapping.emplace_back(ESM::Lockpick::sRecordId ); + mapping.emplace_back(ESM::Repair::sRecordId ); + mapping.emplace_back(ESM::Probe::sRecordId ); assert( std::find(mapping.begin(), mapping.end(), type1) != mapping.end() ); assert( std::find(mapping.begin(), mapping.end(), type2) != mapping.end() ); @@ -62,15 +63,15 @@ namespace float result = 0; // compare items by type - std::string leftName = left.mBase.getTypeName(); - std::string rightName = right.mBase.getTypeName(); + auto leftType = left.mBase.getType(); + auto rightType = right.mBase.getType(); - if (leftName != rightName) - return compareType(leftName, rightName); + if (leftType != rightType) + return compareType(leftType, rightType); // compare items by name - leftName = Misc::StringUtils::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); - rightName = Misc::StringUtils::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); + std::string leftName = Utf8Stream::lowerCaseUtf8(left.mBase.getClass().getName(left.mBase)); + std::string rightName = Utf8Stream::lowerCaseUtf8(right.mBase.getClass().getName(right.mBase)); result = leftName.compare(rightName); if (result != 0) @@ -179,22 +180,22 @@ namespace MWGui MWWorld::Ptr base = item.mBase; int category = 0; - if (base.getTypeName() == typeid(ESM::Armor).name() - || base.getTypeName() == typeid(ESM::Clothing).name()) + if (base.getType() == ESM::Armor::sRecordId + || base.getType() == ESM::Clothing::sRecordId) category = Category_Apparel; - else if (base.getTypeName() == typeid(ESM::Weapon).name()) + else if (base.getType() == ESM::Weapon::sRecordId) category = Category_Weapon; - else if (base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Potion).name()) + else if (base.getType() == ESM::Ingredient::sRecordId + || base.getType() == ESM::Potion::sRecordId) category = Category_Magic; - else if (base.getTypeName() == typeid(ESM::Miscellaneous).name() - || base.getTypeName() == typeid(ESM::Ingredient).name() - || base.getTypeName() == typeid(ESM::Repair).name() - || base.getTypeName() == typeid(ESM::Lockpick).name() - || base.getTypeName() == typeid(ESM::Light).name() - || base.getTypeName() == typeid(ESM::Apparatus).name() - || base.getTypeName() == typeid(ESM::Book).name() - || base.getTypeName() == typeid(ESM::Probe).name()) + else if (base.getType() == ESM::Miscellaneous::sRecordId + || base.getType() == ESM::Ingredient::sRecordId + || base.getType() == ESM::Repair::sRecordId + || base.getType() == ESM::Lockpick::sRecordId + || base.getType() == ESM::Light::sRecordId + || base.getType() == ESM::Apparatus::sRecordId + || base.getType() == ESM::Book::sRecordId + || base.getType() == ESM::Probe::sRecordId) category = Category_Misc; if (item.mFlags & ItemStack::Flag_Enchanted) @@ -205,7 +206,7 @@ namespace MWGui if (mFilter & Filter_OnlyIngredients) { - if (base.getTypeName() != typeid(ESM::Ingredient).name()) + if (base.getType() != ESM::Ingredient::sRecordId) return false; if (!mNameFilter.empty() && !mEffectFilter.empty()) @@ -213,7 +214,7 @@ namespace MWGui if (!mNameFilter.empty()) { - const auto itemName = Misc::StringUtils::lowerCaseUtf8(base.getClass().getName(base)); + const auto itemName = Utf8Stream::lowerCaseUtf8(base.getClass().getName(base)); return itemName.find(mNameFilter) != std::string::npos; } @@ -226,7 +227,7 @@ namespace MWGui for (const auto& effect : effects) { - const auto ciEffect = Misc::StringUtils::lowerCaseUtf8(effect); + const auto ciEffect = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; @@ -238,18 +239,18 @@ namespace MWGui if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; - if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getTypeName() != typeid(ESM::Miscellaneous).name() + if ((mFilter & Filter_OnlyChargedSoulstones) && (base.getType() != ESM::Miscellaneous::sRecordId || base.getCellRef().getSoul() == "" || !MWBase::Environment::get().getWorld()->getStore().get().search(base.getCellRef().getSoul()))) return false; - if ((mFilter & Filter_OnlyRepairTools) && (base.getTypeName() != typeid(ESM::Repair).name())) + if ((mFilter & Filter_OnlyRepairTools) && (base.getType() != ESM::Repair::sRecordId)) return false; if ((mFilter & Filter_OnlyEnchantable) && (item.mFlags & ItemStack::Flag_Enchanted - || (base.getTypeName() != typeid(ESM::Armor).name() - && base.getTypeName() != typeid(ESM::Clothing).name() - && base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Book).name()))) + || (base.getType() != ESM::Armor::sRecordId + && base.getType() != ESM::Clothing::sRecordId + && base.getType() != ESM::Weapon::sRecordId + && base.getType() != ESM::Book::sRecordId))) return false; - if ((mFilter & Filter_OnlyEnchantable) && base.getTypeName() == typeid(ESM::Book).name() + if ((mFilter & Filter_OnlyEnchantable) && base.getType() == ESM::Book::sRecordId && !base.get()->mBase->mData.mIsScroll) return false; @@ -263,8 +264,8 @@ namespace MWGui if ((mFilter & Filter_OnlyRepairable) && ( !base.getClass().hasItemHealth(base) || (base.getClass().getItemHealth(base) == base.getClass().getItemMaxHealth(base)) - || (base.getTypeName() != typeid(ESM::Weapon).name() - && base.getTypeName() != typeid(ESM::Armor).name()))) + || (base.getType() != ESM::Weapon::sRecordId + && base.getType() != ESM::Armor::sRecordId))) return false; if (mFilter & Filter_OnlyRechargable) @@ -285,7 +286,7 @@ namespace MWGui return false; } - std::string compare = Misc::StringUtils::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); + std::string compare = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if(compare.find(mNameFilter) == std::string::npos) return false; @@ -318,12 +319,12 @@ namespace MWGui void SortFilterItemModel::setNameFilter (const std::string& filter) { - mNameFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mNameFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter (const std::string& filter) { - mEffectFilter = Misc::StringUtils::lowerCaseUtf8(filter); + mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::update() diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 61ea9ce93a..455f167415 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -1,6 +1,7 @@ #include "spellmodel.hpp" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -69,7 +70,7 @@ namespace MWGui fullEffectName += " " + wm->getGameSettingString(ESM::Attribute::sGmstAttributeIds[effect.mAttribute], ""); } - std::string convert = Misc::StringUtils::lowerCaseUtf8(fullEffectName); + std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; @@ -90,14 +91,14 @@ namespace MWGui const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); - std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); + std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); for (const ESM::Spell* spell : spells) { if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName); + std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) @@ -139,7 +140,7 @@ namespace MWGui if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; - std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item)); + std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, enchant->mEffects)) diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 8e6f951291..0830af0744 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -599,8 +599,7 @@ namespace MWGui text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { - int rank = factionPair.second; - rank = std::max(0, std::min(9, rank)); + const int rank = std::clamp(factionPair.second, 0, 9); text += std::string("\n#{fontcolourhtml=normal}") + faction->mRanks[rank]; if (rank < 9) diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 7c68798f91..9222e1444d 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -265,6 +265,8 @@ namespace MWGui const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; + // were there any items traded at all? const std::vector& playerBought = playerItemModel->getItemsBorrowedToUs(); const std::vector& merchantBought = mTradeModel->getItemsBorrowedToUs(); @@ -405,10 +407,15 @@ namespace MWGui void TradeWindow::onBalanceValueChanged(int value) { + int previousBalance = mCurrentBalance; + // Entering a "-" sign inverts the buying/selling state mCurrentBalance = (mCurrentBalance >= 0 ? 1 : -1) * value; updateLabels(); + if (mCurrentBalance == 0) + mCurrentBalance = previousBalance; + if (value != std::abs(value)) mTotalBalance->setValue(std::abs(value)); } @@ -418,6 +425,7 @@ namespace MWGui // prevent overflows, and prevent entering INT_MIN since abs(INT_MIN) is undefined if (mCurrentBalance == std::numeric_limits::max() || mCurrentBalance == std::numeric_limits::min()+1) return; + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance -= 1; else mCurrentBalance += 1; updateLabels(); @@ -425,6 +433,7 @@ namespace MWGui void TradeWindow::onDecreaseButtonTriggered() { + if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance += 1; else mCurrentBalance -= 1; updateLabels(); @@ -434,9 +443,17 @@ namespace MWGui { MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); - mPlayerGold->setCaptionWithReplacing("#{sYourGold} " + MyGUI::utility::toString(playerGold)); + TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); + const std::vector& playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); + const std::vector& merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); + + if (playerBorrowed.empty() && merchantBorrowed.empty()) + { + mCurrentBalance = 0; + } + if (mCurrentBalance < 0) { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalCost}"); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 3fc0673735..a42028ee87 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -115,7 +115,7 @@ namespace MWGui std::vector transport; if (mPtr.getClass().isNpc()) transport = mPtr.get()->mBase->getTransport(); - else if (mPtr.getTypeName() == typeid(ESM::Creature).name()) + else if (mPtr.getType() == ESM::Creature::sRecordId) transport = mPtr.get()->mBase->getTransport(); for(unsigned int i = 0;i #include +#include + #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/soundmanager.hpp" @@ -163,6 +165,7 @@ namespace MWGui , mScreenFader(nullptr) , mDebugWindow(nullptr) , mJailScreen(nullptr) + , mContainerWindow(nullptr) , mTranslationDataStorage (translationDataStorage) , mCharGen(nullptr) , mInputBlocker(nullptr) @@ -220,6 +223,7 @@ namespace MWGui ItemWidget::registerComponents(); SpellView::registerComponents(); Gui::registerAllWidgets(); + LuaUi::registerAllWidgets(); MyGUI::FactoryManager::getInstance().registerFactory("Controller"); @@ -357,10 +361,10 @@ namespace MWGui mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow); mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete); - ContainerWindow* containerWindow = new ContainerWindow(mDragAndDrop); - mWindows.push_back(containerWindow); - trackWindow(containerWindow, "container"); - mGuiModeStates[GM_Container] = GuiModeState({containerWindow, mInventoryWindow}); + mContainerWindow = new ContainerWindow(mDragAndDrop); + mWindows.push_back(mContainerWindow); + trackWindow(mContainerWindow, "container"); + mGuiModeStates[GM_Container] = GuiModeState({mContainerWindow, mInventoryWindow}); mHud = new HUD(mCustomMarkers, mDragAndDrop, mLocalMapRender); mWindows.push_back(mHud); @@ -635,6 +639,7 @@ namespace MWGui mMap->setVisible(false); mStatsWindow->setVisible(false); mSpellWindow->setVisible(false); + mHud->setDrowningBarVisible(false); mInventoryWindow->setVisible(getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); } @@ -1095,12 +1100,21 @@ namespace MWGui void WindowManager::windowResized(int x, int y) { - // Note: this is a side effect of resolution change or window resize. - // There is no need to track these changes. Settings::Manager::setInt("resolution x", "Video", x); Settings::Manager::setInt("resolution y", "Video", y); - Settings::Manager::resetPendingChange("resolution x", "Video"); - Settings::Manager::resetPendingChange("resolution y", "Video"); + + // We only want to process changes to window-size related settings. + Settings::CategorySettingVector filter = {{"Video", "resolution x"}, + {"Video", "resolution y"}}; + + // If the HUD has not been initialised, the World singleton will not be available. + if (mHud) + { + MWBase::Environment::get().getWorld()->processChangedSettings( + Settings::Manager::getPendingChanges(filter)); + } + + Settings::Manager::resetPendingChanges(filter); mGuiPlatform->getRenderManagerPtr()->setViewSize(x, y); @@ -1163,6 +1177,16 @@ namespace MWGui } void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) + { + pushGuiMode(mode, arg, false); + } + + void WindowManager::forceLootMode(const MWWorld::Ptr& ptr) + { + pushGuiMode(MWGui::GM_Container, ptr, true); + } + + void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force) { if (mode==GM_Inventory && mAllowed==GW_None) return; @@ -1185,6 +1209,8 @@ namespace MWGui mGuiModeStates[mode].update(true); playSound(mGuiModeStates[mode].mOpenSound); } + if(force) + mContainerWindow->treatNextOpenAsLoot(); for (WindowBase* window : mGuiModeStates[mode].mWindows) window->setPtr(arg); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 9ec79e0c82..f68592cb5d 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -389,6 +389,7 @@ namespace MWGui const std::string& getVersionDescription() const override; void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + void forceLootMode(const MWWorld::Ptr& ptr) override; private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; @@ -447,6 +448,7 @@ namespace MWGui ScreenFader* mScreenFader; DebugWindow* mDebugWindow; JailScreen* mJailScreen; + ContainerWindow* mContainerWindow; std::vector mWindows; @@ -573,6 +575,8 @@ namespace MWGui void enableScene(bool enable); void handleScheduledMessageBoxes(); + + void pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force); }; } diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index e080437d92..59bdc37bb8 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -27,7 +27,6 @@ namespace MWInput { - const float ZOOM_SCALE = 10.f; /// Used for scrolling camera in and out ActionManager::ActionManager(BindingsManager* bindingsManager, osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, @@ -41,7 +40,6 @@ namespace MWInput , mSneaking(false) , mAttemptJump(false) , mOverencumberedMessageDelay(0.f) - , mPreviewPOVDelay(0.f) , mTimeIdle(0.f) { } @@ -109,27 +107,6 @@ namespace MWInput } } - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - const float switchLimit = 0.25; - MWBase::World* world = MWBase::Environment::get().getWorld(); - if (mBindingsManager->actionIsActive(A_TogglePOV)) - { - if (world->isFirstPerson() ? mPreviewPOVDelay > switchLimit : mPreviewPOVDelay == 0) - world->togglePreviewMode(true); - mPreviewPOVDelay += dt; - } - else - { - //disable preview mode - if (mPreviewPOVDelay > 0) - world->togglePreviewMode(false); - if (mPreviewPOVDelay > 0.f && mPreviewPOVDelay <= switchLimit) - world->togglePOV(); - mPreviewPOVDelay = 0.f; - } - } - if (triedToMove) MWBase::Environment::get().getInputManager()->resetIdleTime(); @@ -162,38 +139,16 @@ namespace MWInput resetIdleTime(); } else - { - updateIdleTime(dt); - } + mTimeIdle += dt; mAttemptJump = false; } - - bool ActionManager::isPreviewModeEnabled() - { - return MWBase::Environment::get().getWorld()->isPreviewModeEnabled(); - } void ActionManager::resetIdleTime() { - if (mTimeIdle < 0) - MWBase::Environment::get().getWorld()->toggleVanityMode(false); mTimeIdle = 0.f; } - void ActionManager::updateIdleTime(float dt) - { - static const float vanityDelay = MWBase::Environment::get().getWorld()->getStore().get() - .find("fVanityDelay")->mValue.getFloat(); - if (mTimeIdle >= 0.f) - mTimeIdle += dt; - if (mTimeIdle > vanityDelay) - { - MWBase::Environment::get().getWorld()->toggleVanityMode(true); - mTimeIdle = -1.f; - } - } - void ActionManager::executeAction(int action) { MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::Action, action}); @@ -281,14 +236,6 @@ namespace MWInput case A_ToggleDebug: windowManager->toggleDebugWindow(); break; - case A_ZoomIn: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-ZOOM_SCALE); - break; - case A_ZoomOut: - if (inputManager->getControlSwitch("playerviewswitch") && inputManager->getControlSwitch("playercontrols") && !windowManager->isGuiMode()) - MWBase::Environment::get().getWorld()->adjustCameraDistance(ZOOM_SCALE); - break; case A_QuickSave: quickSave(); break; diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp index 2180e1944e..4141767bcc 100644 --- a/apps/openmw/mwinput/actionmanager.hpp +++ b/apps/openmw/mwinput/actionmanager.hpp @@ -55,13 +55,9 @@ namespace MWInput void setAttemptJump(bool enabled) { mAttemptJump = enabled; } - bool isPreviewModeEnabled(); - private: void handleGuiArrowKey(int action); - void updateIdleTime(float dt); - BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; @@ -72,7 +68,6 @@ namespace MWInput bool mAttemptJump; float mOverencumberedMessageDelay; - float mPreviewPOVDelay; float mTimeIdle; }; } diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index ca7911ecc2..68be849dbd 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -13,7 +15,6 @@ #include "../mwworld/player.hpp" #include "actions.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -546,9 +547,9 @@ namespace MWInput ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; if (mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS::InputControlSystem::UNASSIGNED) - return sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + return SDLUtil::sdlControllerAxisToString(mInputBinder->getJoystickAxisBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else if (mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE) != ICS_MAX_DEVICE_BUTTONS) - return sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); + return SDLUtil::sdlControllerButtonToString(mInputBinder->getJoystickButtonBinding(c, sFakeDeviceId, ICS::Control::INCREASE)); else return "#{sNone}"; } @@ -653,14 +654,13 @@ namespace MWInput return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE); } - float BindingsManager::getControllerAxisValue(SDL_GameControllerAxis axis) const + SDL_GameController* BindingsManager::getControllerOrNull() const { const auto& controllers = mInputBinder->getJoystickInstanceMap(); if (controllers.empty()) - return 0; - SDL_GameController* cntrl = controllers.begin()->second; - constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768; - return SDL_GameControllerGetAxis(cntrl, axis) / static_cast(AXIS_MAX_ABSOLUTE_VALUE); + return nullptr; + else + return controllers.begin()->second; } void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue) diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index 5c653f0b3e..668cccd4ca 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -43,7 +43,8 @@ namespace MWInput bool actionIsActive(int id) const; float getActionValue(int id) const; // returns value in range [0, 1] - float getControllerAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1] + + SDL_GameController* getControllerOrNull() const; void mousePressed(const SDL_MouseButtonEvent &evt, int deviceID); void mouseReleased(const SDL_MouseButtonEvent &arg, int deviceID); diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index fa10ce03cd..1e72a1c95b 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -19,7 +20,6 @@ #include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -34,12 +34,10 @@ namespace MWInput , mJoystickEnabled (Settings::Manager::getBool("enable controller", "Input")) , mGamepadCursorSpeed(Settings::Manager::getFloat("gamepad cursor speed", "Input")) , mSneakToggleShortcutTimer(0.f) - , mGamepadZoom(0) , mGamepadGuiCursorEnabled(true) , mGuiCursorEnabled(true) , mJoystickLastUsed(false) , mSneakGamepadShortcut(false) - , mGamepadPreviewMode(false) { if (!controllerBindingsFile.empty()) { @@ -70,7 +68,7 @@ namespace MWInput } float deadZoneRadius = Settings::Manager::getFloat("joystick dead zone", "Input"); - deadZoneRadius = std::min(std::max(deadZoneRadius, 0.0f), 0.5f); + deadZoneRadius = std::clamp(deadZoneRadius, 0.f, 0.5f); mBindingsManager->setJoystickDeadZone(deadZoneRadius); } @@ -85,8 +83,6 @@ namespace MWInput bool ControllerManager::update(float dt) { - mGamepadPreviewMode = mActionManager->isPreviewModeEnabled(); - if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled)) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f; @@ -115,7 +111,6 @@ namespace MWInput if (MWBase::Environment::get().getWindowManager()->isGuiMode() || MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_Running) { - mGamepadZoom = 0; return false; } @@ -182,15 +177,6 @@ namespace MWInput } } - if (MWBase::Environment::get().getInputManager()->getControlSwitch("playerviewswitch")) - { - if (!mBindingsManager->actionIsActive(A_TogglePOV)) - mGamepadZoom = 0; - - if (mGamepadZoom) - MWBase::Environment::get().getWorld()->adjustCameraDistance(-mGamepadZoom); - } - return triedToMove; } @@ -229,7 +215,7 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); if (!MWBase::Environment::get().getInputManager()->controlsDisabled()) @@ -273,7 +259,7 @@ namespace MWInput mBindingsManager->setPlayerControlsEnabled(true); //esc, to leave initial movie screen - auto kc = sdlKeyToMyGUI(SDLK_ESCAPE); + auto kc = SDLUtil::sdlKeyToMyGUI(SDLK_ESCAPE); mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->controllerButtonReleased(deviceID, arg); @@ -289,21 +275,11 @@ namespace MWInput { gamepadToGuiControl(arg); } - else + else if (MWBase::Environment::get().getWorld()->isPreviewModeEnabled() && + (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)) { - if (mGamepadPreviewMode) // Preview Mode Gamepad Zooming - { - if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) - { - mGamepadZoom = arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - else if (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT) - { - mGamepadZoom = -arg.value * 0.85f / 1000.f / 12.f; - return; // Do not propagate event. - } - } + // Preview Mode Gamepad Zooming; do not propagate to mBindingsManager + return; } mBindingsManager->controllerAxisMoved(deviceID, arg); } @@ -403,4 +379,24 @@ namespace MWInput return true; } + + float ControllerManager::getAxisValue(SDL_GameControllerAxis axis) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + constexpr int AXIS_MAX_ABSOLUTE_VALUE = 32768; + if (cntrl) + return SDL_GameControllerGetAxis(cntrl, axis) / static_cast(AXIS_MAX_ABSOLUTE_VALUE); + else + return 0; + } + + bool ControllerManager::isButtonPressed(SDL_GameControllerButton button) const + { + SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); + if (cntrl) + return SDL_GameControllerGetButton(cntrl, button) > 0; + else + return false; + } + } diff --git a/apps/openmw/mwinput/controllermanager.hpp b/apps/openmw/mwinput/controllermanager.hpp index d8c62d57c4..948b48d538 100644 --- a/apps/openmw/mwinput/controllermanager.hpp +++ b/apps/openmw/mwinput/controllermanager.hpp @@ -34,12 +34,15 @@ namespace MWInput void processChangedSettings(const Settings::CategorySettingVector& changed); void setJoystickLastUsed(bool enabled) { mJoystickLastUsed = enabled; } - bool joystickLastUsed() { return mJoystickLastUsed; } + bool joystickLastUsed() const { return mJoystickLastUsed; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; } - bool gamepadGuiCursorEnabled() { return mGamepadGuiCursorEnabled; } + bool gamepadGuiCursorEnabled() const { return mGamepadGuiCursorEnabled; } + + float getAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1] + bool isButtonPressed(SDL_GameControllerButton button) const; private: // Return true if GUI consumes input. @@ -53,12 +56,10 @@ namespace MWInput bool mJoystickEnabled; float mGamepadCursorSpeed; float mSneakToggleShortcutTimer; - float mGamepadZoom; bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; bool mSneakGamepadShortcut; - bool mGamepadPreviewMode; }; } #endif diff --git a/apps/openmw/mwinput/controlswitch.cpp b/apps/openmw/mwinput/controlswitch.cpp index f31744fca6..6c22e133bc 100644 --- a/apps/openmw/mwinput/controlswitch.cpp +++ b/apps/openmw/mwinput/controlswitch.cpp @@ -29,12 +29,15 @@ namespace MWInput mSwitches["vanitymode"] = true; } - bool ControlSwitch::get(const std::string& key) + bool ControlSwitch::get(std::string_view key) { - return mSwitches[key]; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + return it->second; } - void ControlSwitch::set(const std::string& key, bool value) + void ControlSwitch::set(std::string_view key, bool value) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); @@ -51,15 +54,14 @@ namespace MWInput /// \fixme maybe crouching at this time player.setUpDown(0); } - else if (key == "vanitymode") - { - MWBase::Environment::get().getWorld()->allowVanityMode(value); - } else if (key == "playerlooking" && !value) { MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), osg::Vec3f()); } - mSwitches[key] = value; + auto it = mSwitches.find(key); + if (it == mSwitches.end()) + throw std::runtime_error("Incorrect ControlSwitch: " + std::string(key)); + it->second = value; } void ControlSwitch::write(ESM::ESMWriter& writer, Loading::Listener& /*progress*/) diff --git a/apps/openmw/mwinput/controlswitch.hpp b/apps/openmw/mwinput/controlswitch.hpp index 38d01066bd..b4353c31f5 100644 --- a/apps/openmw/mwinput/controlswitch.hpp +++ b/apps/openmw/mwinput/controlswitch.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace ESM { @@ -23,8 +24,8 @@ namespace MWInput public: ControlSwitch(); - bool get(const std::string& key); - void set(const std::string& key, bool value); + bool get(std::string_view key); + void set(std::string_view key, bool value); void clear(); void write(ESM::ESMWriter& writer, Loading::Listener& progress); @@ -32,7 +33,7 @@ namespace MWInput int countSavedGameRecords() const; private: - std::map mSwitches; + std::map> mSwitches; }; } #endif diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 31f515afb0..4ebe56bf94 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -18,7 +18,6 @@ #include "controlswitch.hpp" #include "keyboardmanager.hpp" #include "mousemanager.hpp" -#include "sdlmappings.hpp" #include "sensormanager.hpp" namespace MWInput @@ -101,8 +100,6 @@ namespace MWInput mMouseManager->update(dt); mSensorManager->update(dt); mActionManager->update(dt, controllerMove); - - MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(dt); } void InputManager::setDragDrop(bool dragDrop) @@ -135,12 +132,12 @@ namespace MWInput mSensorManager->processChangedSettings(changed); } - bool InputManager::getControlSwitch(const std::string& sw) + bool InputManager::getControlSwitch(std::string_view sw) { return mControlSwitch->get(sw); } - void InputManager::toggleControlSwitch(const std::string& sw, bool value) + void InputManager::toggleControlSwitch(std::string_view sw, bool value) { mControlSwitch->set(sw, value); } @@ -180,14 +177,14 @@ namespace MWInput return mBindingsManager->getActionValue(action); } - float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const + bool InputManager::isControllerButtonPressed(SDL_GameControllerButton button) const { - return mBindingsManager->getControllerAxisValue(axis); + return mControllerManager->isButtonPressed(button); } - uint32_t InputManager::getMouseButtonsState() const + float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const { - return mMouseManager->getButtonsState(); + return mControllerManager->getAxisValue(axis); } int InputManager::getMouseMoveX() const diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index adb8319498..41478d5dcb 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -70,8 +70,8 @@ namespace MWInput void setGamepadGuiCursorEnabled(bool enabled) override; void setAttemptJump(bool jumping) override; - void toggleControlSwitch (const std::string& sw, bool value) override; - bool getControlSwitch (const std::string& sw) override; + void toggleControlSwitch(std::string_view sw, bool value) override; + bool getControlSwitch(std::string_view sw) override; std::string getActionDescription (int action) const override; std::string getActionKeyBindingName (int action) const override; @@ -79,8 +79,8 @@ namespace MWInput bool actionIsActive(int action) const override; float getActionValue(int action) const override; + bool isControllerButtonPressed(SDL_GameControllerButton button) const override; float getControllerAxisValue(SDL_GameControllerAxis axis) const override; - uint32_t getMouseButtonsState() const override; int getMouseMoveX() const override; int getMouseMoveY() const override; diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp index b8019b12ba..d8fc548f25 100644 --- a/apps/openmw/mwinput/keyboardmanager.cpp +++ b/apps/openmw/mwinput/keyboardmanager.cpp @@ -4,6 +4,8 @@ #include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/luamanager.hpp" @@ -13,7 +15,6 @@ #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -35,16 +36,16 @@ namespace MWInput // HACK: to make default keybinding for the console work without printing an extra "^" upon closing // This assumes that SDL_TextInput events always come *after* the key event // (which is somewhat reasonable, and hopefully true for all SDL platforms) - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (mBindingsManager->getKeyBinding(A_Console) == arg.keysym.scancode && MWBase::Environment::get().getWindowManager()->isConsoleMode()) SDL_StopTextInput(); bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && - (std::isprint(arg.keysym.sym) || // Don't trust isprint for symbols outside the extended ASCII range - (kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff))); + ((kc == MyGUI::KeyCode::None && arg.keysym.sym > 0xff) || + (arg.keysym.sym >= 0 && arg.keysym.sym <= 255 && std::isprint(arg.keysym.sym)))); if (kc != MyGUI::KeyCode::None && !mBindingsManager->isDetectingBindingState()) { if (MWBase::Environment::get().getWindowManager()->injectKeyPress(kc, 0, arg.repeat)) @@ -71,7 +72,7 @@ namespace MWInput void KeyboardManager::keyReleased(const SDL_KeyboardEvent &arg) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); - auto kc = sdlKeyToMyGUI(arg.keysym.sym); + auto kc = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (!mBindingsManager->isDetectingBindingState()) mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index 7810a40ad2..f646501607 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" @@ -17,7 +18,6 @@ #include "actions.hpp" #include "bindingsmanager.hpp" -#include "sdlmappings.hpp" namespace MWInput { @@ -34,7 +34,6 @@ namespace MWInput , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) - , mButtonsState(0) , mMouseMoveX(0) , mMouseMoveY(0) { @@ -126,7 +125,11 @@ namespace MWInput else { bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; + guiMode = MyGUI::InputManager::getInstance().injectMouseRelease( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(id) + ) && guiMode; if (mBindingsManager->isDetectingBindingState()) return; // don't allow same mouseup to bind as initiated bind @@ -154,7 +157,11 @@ namespace MWInput if (id == SDL_BUTTON_LEFT || id == SDL_BUTTON_RIGHT) // MyGUI only uses these mouse events { guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - guiMode = MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(id)) && guiMode; + guiMode = MyGUI::InputManager::getInstance().injectMousePress( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(id) + ) && guiMode; if (MyGUI::InputManager::getInstance().getMouseFocusWidget () != nullptr) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); @@ -199,7 +206,7 @@ namespace MWInput void MouseManager::update(float dt) { - mButtonsState = SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); + SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); if (!mMouseLookEnabled) return; @@ -230,12 +237,18 @@ namespace MWInput bool MouseManager::injectMouseButtonPress(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMousePress(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMousePress( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(button)); } bool MouseManager::injectMouseButtonRelease(Uint8 button) { - return MyGUI::InputManager::getInstance().injectMouseRelease(static_cast(mGuiCursorX), static_cast(mGuiCursorY), sdlButtonToMyGUI(button)); + return MyGUI::InputManager::getInstance().injectMouseRelease( + static_cast(mGuiCursorX), + static_cast(mGuiCursorY), + SDLUtil::sdlMouseButtonToMyGui(button)); } void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) @@ -245,8 +258,8 @@ namespace MWInput mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mGuiCursorX = std::max(0.f, std::min(mGuiCursorX, float(viewSize.width - 1))); - mGuiCursorY = std::max(0.f, std::min(mGuiCursorY, float(viewSize.height - 1))); + mGuiCursorX = std::clamp(mGuiCursorX, 0.f, viewSize.width - 1); + mGuiCursorY = std::clamp(mGuiCursorY, 0.f, viewSize.height - 1); MyGUI::InputManager::getInstance().injectMouseMove(static_cast(mGuiCursorX), static_cast(mGuiCursorY), static_cast(mMouseWheel)); } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index d5504c5f5a..16ea56d62b 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -38,7 +38,6 @@ namespace MWInput void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } - uint32_t getButtonsState() const { return mButtonsState; } int getMouseMoveX() const { return mMouseMoveX; } int getMouseMoveY() const { return mMouseMoveY; } @@ -58,7 +57,6 @@ namespace MWInput bool mMouseLookEnabled; bool mGuiCursorEnabled; - uint32_t mButtonsState; int mMouseMoveX; int mMouseMoveY; }; diff --git a/apps/openmw/mwinput/sdlmappings.cpp b/apps/openmw/mwinput/sdlmappings.cpp deleted file mode 100644 index 0c3f5c5d85..0000000000 --- a/apps/openmw/mwinput/sdlmappings.cpp +++ /dev/null @@ -1,218 +0,0 @@ -#include "sdlmappings.hpp" - -#include - -#include - -#include -#include - -namespace MWInput -{ - std::string sdlControllerButtonToString(int button) - { - switch(button) - { - case SDL_CONTROLLER_BUTTON_A: - return "A Button"; - case SDL_CONTROLLER_BUTTON_B: - return "B Button"; - case SDL_CONTROLLER_BUTTON_BACK: - return "Back Button"; - case SDL_CONTROLLER_BUTTON_DPAD_DOWN: - return "DPad Down"; - case SDL_CONTROLLER_BUTTON_DPAD_LEFT: - return "DPad Left"; - case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: - return "DPad Right"; - case SDL_CONTROLLER_BUTTON_DPAD_UP: - return "DPad Up"; - case SDL_CONTROLLER_BUTTON_GUIDE: - return "Guide Button"; - case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - return "Left Shoulder"; - case SDL_CONTROLLER_BUTTON_LEFTSTICK: - return "Left Stick Button"; - case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - return "Right Shoulder"; - case SDL_CONTROLLER_BUTTON_RIGHTSTICK: - return "Right Stick Button"; - case SDL_CONTROLLER_BUTTON_START: - return "Start Button"; - case SDL_CONTROLLER_BUTTON_X: - return "X Button"; - case SDL_CONTROLLER_BUTTON_Y: - return "Y Button"; - default: - return "Button " + std::to_string(button); - } - } - - std::string sdlControllerAxisToString(int axis) - { - switch(axis) - { - case SDL_CONTROLLER_AXIS_LEFTX: - return "Left Stick X"; - case SDL_CONTROLLER_AXIS_LEFTY: - return "Left Stick Y"; - case SDL_CONTROLLER_AXIS_RIGHTX: - return "Right Stick X"; - case SDL_CONTROLLER_AXIS_RIGHTY: - return "Right Stick Y"; - case SDL_CONTROLLER_AXIS_TRIGGERLEFT: - return "Left Trigger"; - case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: - return "Right Trigger"; - default: - return "Axis " + std::to_string(axis); - } - } - - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button) - { - //The right button is the second button, according to MyGUI - if(button == SDL_BUTTON_RIGHT) - button = SDL_BUTTON_MIDDLE; - else if(button == SDL_BUTTON_MIDDLE) - button = SDL_BUTTON_RIGHT; - - //MyGUI's buttons are 0 indexed - return MyGUI::MouseButton::Enum(button - 1); - } - - void initKeyMap(std::map& keyMap) - { - keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None; - keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape; - keyMap[SDLK_1] = MyGUI::KeyCode::One; - keyMap[SDLK_2] = MyGUI::KeyCode::Two; - keyMap[SDLK_3] = MyGUI::KeyCode::Three; - keyMap[SDLK_4] = MyGUI::KeyCode::Four; - keyMap[SDLK_5] = MyGUI::KeyCode::Five; - keyMap[SDLK_6] = MyGUI::KeyCode::Six; - keyMap[SDLK_7] = MyGUI::KeyCode::Seven; - keyMap[SDLK_8] = MyGUI::KeyCode::Eight; - keyMap[SDLK_9] = MyGUI::KeyCode::Nine; - keyMap[SDLK_0] = MyGUI::KeyCode::Zero; - keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus; - keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals; - keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace; - keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab; - keyMap[SDLK_q] = MyGUI::KeyCode::Q; - keyMap[SDLK_w] = MyGUI::KeyCode::W; - keyMap[SDLK_e] = MyGUI::KeyCode::E; - keyMap[SDLK_r] = MyGUI::KeyCode::R; - keyMap[SDLK_t] = MyGUI::KeyCode::T; - keyMap[SDLK_y] = MyGUI::KeyCode::Y; - keyMap[SDLK_u] = MyGUI::KeyCode::U; - keyMap[SDLK_i] = MyGUI::KeyCode::I; - keyMap[SDLK_o] = MyGUI::KeyCode::O; - keyMap[SDLK_p] = MyGUI::KeyCode::P; - keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return; - keyMap[SDLK_a] = MyGUI::KeyCode::A; - keyMap[SDLK_s] = MyGUI::KeyCode::S; - keyMap[SDLK_d] = MyGUI::KeyCode::D; - keyMap[SDLK_f] = MyGUI::KeyCode::F; - keyMap[SDLK_g] = MyGUI::KeyCode::G; - keyMap[SDLK_h] = MyGUI::KeyCode::H; - keyMap[SDLK_j] = MyGUI::KeyCode::J; - keyMap[SDLK_k] = MyGUI::KeyCode::K; - keyMap[SDLK_l] = MyGUI::KeyCode::L; - keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon; - keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe; - keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave; - keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift; - keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash; - keyMap[SDLK_z] = MyGUI::KeyCode::Z; - keyMap[SDLK_x] = MyGUI::KeyCode::X; - keyMap[SDLK_c] = MyGUI::KeyCode::C; - keyMap[SDLK_v] = MyGUI::KeyCode::V; - keyMap[SDLK_b] = MyGUI::KeyCode::B; - keyMap[SDLK_n] = MyGUI::KeyCode::N; - keyMap[SDLK_m] = MyGUI::KeyCode::M; - keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma; - keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period; - keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash; - keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift; - keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply; - keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt; - keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space; - keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital; - keyMap[SDLK_F1] = MyGUI::KeyCode::F1; - keyMap[SDLK_F2] = MyGUI::KeyCode::F2; - keyMap[SDLK_F3] = MyGUI::KeyCode::F3; - keyMap[SDLK_F4] = MyGUI::KeyCode::F4; - keyMap[SDLK_F5] = MyGUI::KeyCode::F5; - keyMap[SDLK_F6] = MyGUI::KeyCode::F6; - keyMap[SDLK_F7] = MyGUI::KeyCode::F7; - keyMap[SDLK_F8] = MyGUI::KeyCode::F8; - keyMap[SDLK_F9] = MyGUI::KeyCode::F9; - keyMap[SDLK_F10] = MyGUI::KeyCode::F10; - keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock; - keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock; - keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7; - keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8; - keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9; - keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract; - keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4; - keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5; - keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6; - keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add; - keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1; - keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2; - keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3; - keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0; - keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal; - keyMap[SDLK_F11] = MyGUI::KeyCode::F11; - keyMap[SDLK_F12] = MyGUI::KeyCode::F12; - keyMap[SDLK_F13] = MyGUI::KeyCode::F13; - keyMap[SDLK_F14] = MyGUI::KeyCode::F14; - keyMap[SDLK_F15] = MyGUI::KeyCode::F15; - keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals; - keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon; - keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter; - keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide; - keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq; - keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt; - keyMap[SDLK_HOME] = MyGUI::KeyCode::Home; - keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp; - keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp; - keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft; - keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight; - keyMap[SDLK_END] = MyGUI::KeyCode::End; - keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown; - keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown; - keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert; - keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete; - keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu; - -//The function of the Ctrl and Meta keys are switched on macOS compared to other platforms. -//For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard -#if defined(__APPLE__) - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows; -#else - keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows; - keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows; - keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl; - keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl; -#endif - } - - MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code) - { - static std::map keyMap; - if (keyMap.empty()) - initKeyMap(keyMap); - - MyGUI::KeyCode kc = MyGUI::KeyCode::None; - auto foundKey = keyMap.find(code); - if (foundKey != keyMap.end()) - kc = foundKey->second; - - return kc; - } -} diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index aad70d1a57..49bb5cfcb4 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -1,6 +1,10 @@ #include "actions.hpp" +#include + #include +#include +#include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -9,15 +13,35 @@ namespace MWLua { + Action::Action(LuaUtil::LuaState* state) + { + static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); + if (luaDebug) + mCallerTraceback = state->debugTraceback(); + } + + void Action::safeApply(WorldView& w) const + { + try + { + apply(w); + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in " << this->toString() << ": " << e.what(); + + if (mCallerTraceback.empty()) + Log(Debug::Error) << "Set 'lua_debug=true' in settings.cfg to enable action tracebacks"; + else + Log(Debug::Error) << "Caller " << mCallerTraceback; + } + } void TeleportAction::apply(WorldView& worldView) const { MWWorld::CellStore* cell = worldView.findCell(mCell, mPos); if (!cell) - { - Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'"; - return; - } + throw std::runtime_error(std::string("cell not found: '") + mCell + "'"); MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false); diff --git a/apps/openmw/mwlua/actions.hpp b/apps/openmw/mwlua/actions.hpp index 900b175320..e88b39e83c 100644 --- a/apps/openmw/mwlua/actions.hpp +++ b/apps/openmw/mwlua/actions.hpp @@ -6,6 +6,11 @@ #include "object.hpp" #include "worldview.hpp" +namespace LuaUtil +{ + class LuaState; +} + namespace MWLua { @@ -16,17 +21,25 @@ namespace MWLua class Action { public: + Action(LuaUtil::LuaState* state); virtual ~Action() {} + + void safeApply(WorldView&) const; virtual void apply(WorldView&) const = 0; + virtual std::string toString() const = 0; + + private: + std::string mCallerTraceback; }; class TeleportAction final : public Action { public: - TeleportAction(ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot) - : mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {} + TeleportAction(LuaUtil::LuaState* state, ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot) + : Action(state), mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {} void apply(WorldView&) const override; + std::string toString() const override { return "TeleportAction"; } private: ObjectId mObject; @@ -41,9 +54,11 @@ namespace MWLua using Item = std::variant; // recordId or ObjectId using Equipment = std::map; // slot to item - SetEquipmentAction(ObjectId actor, Equipment equipment) : mActor(actor), mEquipment(std::move(equipment)) {} + SetEquipmentAction(LuaUtil::LuaState* state, ObjectId actor, Equipment equipment) + : Action(state), mActor(actor), mEquipment(std::move(equipment)) {} void apply(WorldView&) const override; + std::string toString() const override { return "SetEquipmentAction"; } private: ObjectId mActor; diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 9fdda53d9d..9bddf75ee4 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -23,7 +23,7 @@ namespace MWLua sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) { - asyncId.mContainer->registerTimerCallback(asyncId.mScript, name, std::move(callback)); + asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); return TimerCallback{asyncId, std::string(name)}; }; api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, @@ -31,35 +31,34 @@ namespace MWLua { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, - callback.mAsyncId.mScript, callback.mName, std::move(callbackArg)); + callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback)); }; api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); + TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback)); }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { - return Callback{std::move(fn), asyncId.mHiddenData}; + return LuaUtil::Callback{std::move(fn), asyncId.mHiddenData}; }; auto initializer = [](sol::table hiddenData) { - LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; - hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); - return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; + LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey]; + return AsyncPackageId{id.mContainer, id.mIndex, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp index 46bf079d65..abbd806cef 100644 --- a/apps/openmw/mwlua/camerabindings.cpp +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -1,12 +1,82 @@ #include "luabindings.hpp" +#include "../mwrender/camera.hpp" + namespace MWLua { + using CameraMode = MWRender::Camera::Mode; + sol::table initCameraPackage(const Context& context) { + MWRender::Camera* camera = MWBase::Environment::get().getWorld()->getCamera(); + sol::table api(context.mLua->sol(), sol::create); - // TODO + api["MODE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( + "Static", CameraMode::Static, + "FirstPerson", CameraMode::FirstPerson, + "ThirdPerson", CameraMode::ThirdPerson, + "Vanity", CameraMode::Vanity, + "Preview", CameraMode::Preview + )); + + api["getMode"] = [camera]() -> int { return static_cast(camera->getMode()); }; + api["getQueuedMode"] = [camera]() -> sol::optional + { + std::optional mode = camera->getQueuedMode(); + if (mode) + return static_cast(*mode); + else + return sol::nullopt; + }; + api["setMode"] = [camera](int mode, sol::optional force) + { + camera->setMode(static_cast(mode), force ? *force : false); + }; + + api["allowCharacterDeferredRotation"] = [camera](bool v) { camera->allowCharacterDeferredRotation(v); }; + api["showCrosshair"] = [camera](bool v) { camera->showCrosshair(v); }; + + api["getTrackedPosition"] = [camera]() -> osg::Vec3f { return camera->getTrackedPosition(); }; + api["getPosition"] = [camera]() -> osg::Vec3f { return camera->getPosition(); }; + + // All angles are negated in order to make camera rotation consistent with objects rotation. + // TODO: Fix the inconsistency of rotation direction in camera.cpp. + api["getPitch"] = [camera]() { return -camera->getPitch(); }; + api["getYaw"] = [camera]() { return -camera->getYaw(); }; + api["getRoll"] = [camera]() { return -camera->getRoll(); }; + + api["setStaticPosition"] = [camera](const osg::Vec3f& pos) { camera->setStaticPosition(pos); }; + api["setPitch"] = [camera](float v) + { + camera->setPitch(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setYaw"] = [camera](float v) + { + camera->setYaw(-v, true); + if (camera->getMode() == CameraMode::ThirdPerson) + camera->calculateDeferredRotation(); + }; + api["setRoll"] = [camera](float v) { camera->setRoll(-v); }; + api["setExtraPitch"] = [camera](float v) { camera->setExtraPitch(-v); }; + api["setExtraYaw"] = [camera](float v) { camera->setExtraYaw(-v); }; + api["getExtraPitch"] = [camera]() { return -camera->getExtraPitch(); }; + api["getExtraYaw"] = [camera]() { return -camera->getExtraYaw(); }; + + api["getThirdPersonDistance"] = [camera]() { return camera->getCameraDistance(); }; + api["setPreferredThirdPersonDistance"] = [camera](float v) { camera->setPreferredCameraDistance(v); }; + + api["getFirstPersonOffset"] = [camera]() { return camera->getFirstPersonOffset(); }; + api["setFirstPersonOffset"] = [camera](const osg::Vec3f& v) { camera->setFirstPersonOffset(v); }; + + api["getFocalPreferredOffset"] = [camera]() -> osg::Vec2f { return camera->getFocalPointTargetOffset(); }; + api["setFocalPreferredOffset"] = [camera](const osg::Vec2f& v) { camera->setFocalPointTargetOffset(v); }; + api["getFocalTransitionSpeed"] = [camera]() { return camera->getFocalPointTransitionSpeed(); }; + api["setFocalTransitionSpeed"] = [camera](float v) { camera->setFocalPointTransitionSpeed(v); }; + api["instantTransition"] = [camera]() { camera->instantTransition(); }; + return LuaUtil::makeReadOnly(api); } diff --git a/apps/openmw/mwlua/globalscripts.hpp b/apps/openmw/mwlua/globalscripts.hpp index 9a371809ac..2737dabaca 100644 --- a/apps/openmw/mwlua/globalscripts.hpp +++ b/apps/openmw/mwlua/globalscripts.hpp @@ -16,7 +16,8 @@ namespace MWLua class GlobalScripts : public LuaUtil::ScriptsContainer { public: - GlobalScripts(LuaUtil::LuaState* lua) : LuaUtil::ScriptsContainer(lua, "Global") + GlobalScripts(LuaUtil::LuaState* lua) : + LuaUtil::ScriptsContainer(lua, "Global", ESM::LuaScriptCfg::sGlobal) { registerEngineHandlers({&mActorActiveHandlers, &mNewGameHandlers, &mPlayerAddedHandlers}); } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp index aaa00f3da9..8aecd1269c 100644 --- a/apps/openmw/mwlua/inputbindings.cpp +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "../mwbase/inputmanager.hpp" #include "../mwinput/actions.hpp" @@ -18,9 +19,14 @@ namespace MWLua sol::table initInputPackage(const Context& context) { sol::usertype keyEvent = context.mLua->sol().new_usertype("KeyEvent"); - keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { return std::string(1, static_cast(e.sym)); }); - keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.sym; }); - keyEvent["modifiers"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.mod; }); + keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) + { + if (e.sym > 0 && e.sym <= 255) + return std::string(1, static_cast(e.sym)); + else + return std::string(); + }); + keyEvent["code"] = sol::readonly_property([](const SDL_Keysym& e) -> int { return e.scancode; }); keyEvent["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); @@ -31,9 +37,26 @@ namespace MWLua api["isIdle"] = [input]() { return input->isIdle(); }; api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; + api["isKeyPressed"] = [](SDL_Scancode code) -> bool + { + int maxCode; + const auto* state = SDL_GetKeyboardState(&maxCode); + if (code >= 0 && code < maxCode) + return state[code] != 0; + else + return false; + }; + api["isShiftPressed"] = []() -> bool { return SDL_GetModState() & KMOD_SHIFT; }; + api["isCtrlPressed"] = []() -> bool { return SDL_GetModState() & KMOD_CTRL; }; + api["isAltPressed"] = []() -> bool { return SDL_GetModState() & KMOD_ALT; }; + api["isSuperPressed"] = []() -> bool { return SDL_GetModState() & KMOD_GUI; }; + api["isControllerButtonPressed"] = [input](int button) + { + return input->isControllerButtonPressed(static_cast(button)); + }; api["isMouseButtonPressed"] = [input](int button) -> bool { - return input->getMouseButtonsState() & (1 << (button - 1)); + return SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button); }; api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); }; api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); }; @@ -45,104 +68,219 @@ namespace MWLua return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1; }; - api["getControlSwitch"] = [input](const std::string& key) { return input->getControlSwitch(key); }; - api["setControlSwitch"] = [input](const std::string& key, bool v) { input->toggleControlSwitch(key, v); }; - - api["ACTION"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( - "GameMenu", MWInput::A_GameMenu, - "Screenshot", MWInput::A_Screenshot, - "Inventory", MWInput::A_Inventory, - "Console", MWInput::A_Console, - - "MoveLeft", MWInput::A_MoveLeft, - "MoveRight", MWInput::A_MoveRight, - "MoveForward", MWInput::A_MoveForward, - "MoveBackward", MWInput::A_MoveBackward, - - "Activate", MWInput::A_Activate, - "Use", MWInput::A_Use, - "Jump", MWInput::A_Jump, - "AutoMove", MWInput::A_AutoMove, - "Rest", MWInput::A_Rest, - "Journal", MWInput::A_Journal, - "Weapon", MWInput::A_Weapon, - "Spell", MWInput::A_Spell, - "Run", MWInput::A_Run, - "CycleSpellLeft", MWInput::A_CycleSpellLeft, - "CycleSpellRight", MWInput::A_CycleSpellRight, - "CycleWeaponLeft", MWInput::A_CycleWeaponLeft, - "CycleWeaponRight", MWInput::A_CycleWeaponRight, - "ToggleSneak", MWInput::A_ToggleSneak, - "AlwaysRun", MWInput::A_AlwaysRun, - "Sneak", MWInput::A_Sneak, - - "QuickSave", MWInput::A_QuickSave, - "QuickLoad", MWInput::A_QuickLoad, - "QuickMenu", MWInput::A_QuickMenu, - "ToggleWeapon", MWInput::A_ToggleWeapon, - "ToggleSpell", MWInput::A_ToggleSpell, - "TogglePOV", MWInput::A_TogglePOV, - - "QuickKey1", MWInput::A_QuickKey1, - "QuickKey2", MWInput::A_QuickKey2, - "QuickKey3", MWInput::A_QuickKey3, - "QuickKey4", MWInput::A_QuickKey4, - "QuickKey5", MWInput::A_QuickKey5, - "QuickKey6", MWInput::A_QuickKey6, - "QuickKey7", MWInput::A_QuickKey7, - "QuickKey8", MWInput::A_QuickKey8, - "QuickKey9", MWInput::A_QuickKey9, - "QuickKey10", MWInput::A_QuickKey10, - "QuickKeysMenu", MWInput::A_QuickKeysMenu, - - "ToggleHUD", MWInput::A_ToggleHUD, - "ToggleDebug", MWInput::A_ToggleDebug, - - "ZoomIn", MWInput::A_ZoomIn, - "ZoomOut", MWInput::A_ZoomOut - )); - - api["CONTROL_SWITCH"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( - "Controls", "playercontrols", - "Fighting", "playerfighting", - "Jumping", "playerjumping", - "Looking", "playerlooking", - "Magic", "playermagic", - "ViewMode", "playerviewswitch", - "VanityMode", "vanitymode" - )); - - api["CONTROLLER_BUTTON"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( - "A", SDL_CONTROLLER_BUTTON_A, - "B", SDL_CONTROLLER_BUTTON_B, - "X", SDL_CONTROLLER_BUTTON_X, - "Y", SDL_CONTROLLER_BUTTON_Y, - "Back", SDL_CONTROLLER_BUTTON_BACK, - "Guide", SDL_CONTROLLER_BUTTON_GUIDE, - "Start", SDL_CONTROLLER_BUTTON_START, - "LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK, - "RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK, - "LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER, - "RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, - "DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP, - "DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN, - "DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT, - "DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT - )); - - api["CONTROLLER_AXIS"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( - "LeftX", SDL_CONTROLLER_AXIS_LEFTX, - "LeftY", SDL_CONTROLLER_AXIS_LEFTY, - "RightX", SDL_CONTROLLER_AXIS_RIGHTX, - "RightY", SDL_CONTROLLER_AXIS_RIGHTY, - "TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT, - "TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT, - - "LookUpDown", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookUpDown, - "LookLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookLeftRight, - "MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveForwardBackward, - "MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveLeftRight - )); + api["getControlSwitch"] = [input](std::string_view key) { return input->getControlSwitch(key); }; + api["setControlSwitch"] = [input](std::string_view key, bool v) { input->toggleControlSwitch(key, v); }; + + api["getKeyName"] = [](SDL_Scancode code) { + return SDL_GetKeyName(SDL_GetKeyFromScancode(code)); + }; + + api["ACTION"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"GameMenu", MWInput::A_GameMenu}, + {"Screenshot", MWInput::A_Screenshot}, + {"Inventory", MWInput::A_Inventory}, + {"Console", MWInput::A_Console}, + + {"MoveLeft", MWInput::A_MoveLeft}, + {"MoveRight", MWInput::A_MoveRight}, + {"MoveForward", MWInput::A_MoveForward}, + {"MoveBackward", MWInput::A_MoveBackward}, + + {"Activate", MWInput::A_Activate}, + {"Use", MWInput::A_Use}, + {"Jump", MWInput::A_Jump}, + {"AutoMove", MWInput::A_AutoMove}, + {"Rest", MWInput::A_Rest}, + {"Journal", MWInput::A_Journal}, + {"Weapon", MWInput::A_Weapon}, + {"Spell", MWInput::A_Spell}, + {"Run", MWInput::A_Run}, + {"CycleSpellLeft", MWInput::A_CycleSpellLeft}, + {"CycleSpellRight", MWInput::A_CycleSpellRight}, + {"CycleWeaponLeft", MWInput::A_CycleWeaponLeft}, + {"CycleWeaponRight", MWInput::A_CycleWeaponRight}, + {"ToggleSneak", MWInput::A_ToggleSneak}, + {"AlwaysRun", MWInput::A_AlwaysRun}, + {"Sneak", MWInput::A_Sneak}, + + {"QuickSave", MWInput::A_QuickSave}, + {"QuickLoad", MWInput::A_QuickLoad}, + {"QuickMenu", MWInput::A_QuickMenu}, + {"ToggleWeapon", MWInput::A_ToggleWeapon}, + {"ToggleSpell", MWInput::A_ToggleSpell}, + {"TogglePOV", MWInput::A_TogglePOV}, + + {"QuickKey1", MWInput::A_QuickKey1}, + {"QuickKey2", MWInput::A_QuickKey2}, + {"QuickKey3", MWInput::A_QuickKey3}, + {"QuickKey4", MWInput::A_QuickKey4}, + {"QuickKey5", MWInput::A_QuickKey5}, + {"QuickKey6", MWInput::A_QuickKey6}, + {"QuickKey7", MWInput::A_QuickKey7}, + {"QuickKey8", MWInput::A_QuickKey8}, + {"QuickKey9", MWInput::A_QuickKey9}, + {"QuickKey10", MWInput::A_QuickKey10}, + {"QuickKeysMenu", MWInput::A_QuickKeysMenu}, + + {"ToggleHUD", MWInput::A_ToggleHUD}, + {"ToggleDebug", MWInput::A_ToggleDebug}, + + {"ZoomIn", MWInput::A_ZoomIn}, + {"ZoomOut", MWInput::A_ZoomOut} + })); + + api["CONTROL_SWITCH"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"Controls", "playercontrols"}, + {"Fighting", "playerfighting"}, + {"Jumping", "playerjumping"}, + {"Looking", "playerlooking"}, + {"Magic", "playermagic"}, + {"ViewMode", "playerviewswitch"}, + {"VanityMode", "vanitymode"} + })); + + api["CONTROLLER_BUTTON"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"A", SDL_CONTROLLER_BUTTON_A}, + {"B", SDL_CONTROLLER_BUTTON_B}, + {"X", SDL_CONTROLLER_BUTTON_X}, + {"Y", SDL_CONTROLLER_BUTTON_Y}, + {"Back", SDL_CONTROLLER_BUTTON_BACK}, + {"Guide", SDL_CONTROLLER_BUTTON_GUIDE}, + {"Start", SDL_CONTROLLER_BUTTON_START}, + {"LeftStick", SDL_CONTROLLER_BUTTON_LEFTSTICK}, + {"RightStick", SDL_CONTROLLER_BUTTON_RIGHTSTICK}, + {"LeftShoulder", SDL_CONTROLLER_BUTTON_LEFTSHOULDER}, + {"RightShoulder", SDL_CONTROLLER_BUTTON_RIGHTSHOULDER}, + {"DPadUp", SDL_CONTROLLER_BUTTON_DPAD_UP}, + {"DPadDown", SDL_CONTROLLER_BUTTON_DPAD_DOWN}, + {"DPadLeft", SDL_CONTROLLER_BUTTON_DPAD_LEFT}, + {"DPadRight", SDL_CONTROLLER_BUTTON_DPAD_RIGHT} + })); + + api["CONTROLLER_AXIS"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"LeftX", SDL_CONTROLLER_AXIS_LEFTX}, + {"LeftY", SDL_CONTROLLER_AXIS_LEFTY}, + {"RightX", SDL_CONTROLLER_AXIS_RIGHTX}, + {"RightY", SDL_CONTROLLER_AXIS_RIGHTY}, + {"TriggerLeft", SDL_CONTROLLER_AXIS_TRIGGERLEFT}, + {"TriggerRight", SDL_CONTROLLER_AXIS_TRIGGERRIGHT}, + + {"LookUpDown", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookUpDown}, + {"LookLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_LookLeftRight}, + {"MoveForwardBackward", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveForwardBackward}, + {"MoveLeftRight", SDL_CONTROLLER_AXIS_MAX + MWInput::A_MoveLeftRight} + })); + + api["KEY"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"_0", SDL_SCANCODE_0}, + {"_1", SDL_SCANCODE_1}, + {"_2", SDL_SCANCODE_2}, + {"_3", SDL_SCANCODE_3}, + {"_4", SDL_SCANCODE_4}, + {"_5", SDL_SCANCODE_5}, + {"_6", SDL_SCANCODE_6}, + {"_7", SDL_SCANCODE_7}, + {"_8", SDL_SCANCODE_8}, + {"_9", SDL_SCANCODE_9}, + + {"NP_0", SDL_SCANCODE_KP_0}, + {"NP_1", SDL_SCANCODE_KP_1}, + {"NP_2", SDL_SCANCODE_KP_2}, + {"NP_3", SDL_SCANCODE_KP_3}, + {"NP_4", SDL_SCANCODE_KP_4}, + {"NP_5", SDL_SCANCODE_KP_5}, + {"NP_6", SDL_SCANCODE_KP_6}, + {"NP_7", SDL_SCANCODE_KP_7}, + {"NP_8", SDL_SCANCODE_KP_8}, + {"NP_9", SDL_SCANCODE_KP_9}, + {"NP_Divide", SDL_SCANCODE_KP_DIVIDE}, + {"NP_Enter", SDL_SCANCODE_KP_ENTER}, + {"NP_Minus", SDL_SCANCODE_KP_MINUS}, + {"NP_Multiply", SDL_SCANCODE_KP_MULTIPLY}, + {"NP_Delete", SDL_SCANCODE_KP_PERIOD}, + {"NP_Plus", SDL_SCANCODE_KP_PLUS}, + + {"F1", SDL_SCANCODE_F1}, + {"F2", SDL_SCANCODE_F2}, + {"F3", SDL_SCANCODE_F3}, + {"F4", SDL_SCANCODE_F4}, + {"F5", SDL_SCANCODE_F5}, + {"F6", SDL_SCANCODE_F6}, + {"F7", SDL_SCANCODE_F7}, + {"F8", SDL_SCANCODE_F8}, + {"F9", SDL_SCANCODE_F9}, + {"F10", SDL_SCANCODE_F10}, + {"F11", SDL_SCANCODE_F11}, + {"F12", SDL_SCANCODE_F12}, + + {"A", SDL_SCANCODE_A}, + {"B", SDL_SCANCODE_B}, + {"C", SDL_SCANCODE_C}, + {"D", SDL_SCANCODE_D}, + {"E", SDL_SCANCODE_E}, + {"F", SDL_SCANCODE_F}, + {"G", SDL_SCANCODE_G}, + {"H", SDL_SCANCODE_H}, + {"I", SDL_SCANCODE_I}, + {"J", SDL_SCANCODE_J}, + {"K", SDL_SCANCODE_K}, + {"L", SDL_SCANCODE_L}, + {"M", SDL_SCANCODE_M}, + {"N", SDL_SCANCODE_N}, + {"O", SDL_SCANCODE_O}, + {"P", SDL_SCANCODE_P}, + {"Q", SDL_SCANCODE_Q}, + {"R", SDL_SCANCODE_R}, + {"S", SDL_SCANCODE_S}, + {"T", SDL_SCANCODE_T}, + {"U", SDL_SCANCODE_U}, + {"V", SDL_SCANCODE_V}, + {"W", SDL_SCANCODE_W}, + {"X", SDL_SCANCODE_X}, + {"Y", SDL_SCANCODE_Y}, + {"Z", SDL_SCANCODE_Z}, + + {"LeftArrow", SDL_SCANCODE_LEFT}, + {"RightArrow", SDL_SCANCODE_RIGHT}, + {"UpArrow", SDL_SCANCODE_UP}, + {"DownArrow", SDL_SCANCODE_DOWN}, + + {"LeftAlt", SDL_SCANCODE_LALT}, + {"LeftCtrl", SDL_SCANCODE_LCTRL}, + {"LeftBracket", SDL_SCANCODE_LEFTBRACKET}, + {"LeftSuper", SDL_SCANCODE_LGUI}, + {"LeftShift", SDL_SCANCODE_LSHIFT}, + {"RightAlt", SDL_SCANCODE_RALT}, + {"RightCtrl", SDL_SCANCODE_RCTRL}, + {"RightSuper", SDL_SCANCODE_RGUI}, + {"RightBracket", SDL_SCANCODE_RIGHTBRACKET}, + {"RightShift", SDL_SCANCODE_RSHIFT}, + + {"Apostrophe", SDL_SCANCODE_APOSTROPHE}, + {"BackSlash", SDL_SCANCODE_BACKSLASH}, + {"Backspace", SDL_SCANCODE_BACKSPACE}, + {"CapsLock", SDL_SCANCODE_CAPSLOCK}, + {"Comma", SDL_SCANCODE_COMMA}, + {"Delete", SDL_SCANCODE_DELETE}, + {"End", SDL_SCANCODE_END}, + {"Enter", SDL_SCANCODE_RETURN}, + {"Equals", SDL_SCANCODE_EQUALS}, + {"Escape", SDL_SCANCODE_ESCAPE}, + {"Home", SDL_SCANCODE_HOME}, + {"Insert", SDL_SCANCODE_INSERT}, + {"Minus", SDL_SCANCODE_MINUS}, + {"NumLock", SDL_SCANCODE_NUMLOCKCLEAR}, + {"PageDown", SDL_SCANCODE_PAGEDOWN}, + {"PageUp", SDL_SCANCODE_PAGEUP}, + {"Period", SDL_SCANCODE_PERIOD}, + {"Pause", SDL_SCANCODE_PAUSE}, + {"PrintScreen", SDL_SCANCODE_PRINTSCREEN}, + {"ScrollLock", SDL_SCANCODE_SCROLLLOCK}, + {"Semicolon", SDL_SCANCODE_SEMICOLON}, + {"Slash", SDL_SCANCODE_SLASH}, + {"Space", SDL_SCANCODE_SPACE}, + {"Tab", SDL_SCANCODE_TAB} + })); return LuaUtil::makeReadOnly(api); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8a1b76a8ce..98cf0a0b1a 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -39,7 +39,7 @@ namespace MWLua selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; }; selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; }; - selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment) + selfAPI["setEquipment"] = [context](const SelfObject& obj, sol::table equipment) { if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) { @@ -56,7 +56,7 @@ namespace MWLua else eqp[slot] = value.as(); } - manager->addAction(std::make_unique(obj.id(), std::move(eqp))); + context.mLuaManager->addAction(std::make_unique(context.mLua, obj.id(), std::move(eqp))); }; selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional { @@ -82,14 +82,14 @@ namespace MWLua }; } - LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) - : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) + LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode) + : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id()), autoStartMode), mData(obj) { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); } - void LocalScripts::receiveEngineEvent(const EngineEvent& event, ObjectRegistry*) + void LocalScripts::receiveEngineEvent(const EngineEvent& event) { std::visit([this](auto&& arg) { diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index 80d04b7a40..68da0b8b03 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -20,7 +20,7 @@ namespace MWLua { public: static void initializeSelfPackage(const Context&); - LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); + LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } @@ -39,7 +39,7 @@ namespace MWLua }; using EngineEvent = std::variant; - void receiveEngineEvent(const EngineEvent&, ObjectRegistry*); + void receiveEngineEvent(const EngineEvent&); protected: SelfObject mData; diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index aceffc24db..80af77139c 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -25,11 +25,10 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 7; + api["API_REVISION"] = 10; api["quit"] = [lua]() { - std::string traceback = lua->sol()["debug"]["traceback"]().get(); - Log(Debug::Warning) << "Quit requested by a Lua script.\n" << traceback; + Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); }; api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) @@ -43,27 +42,27 @@ namespace MWLua "Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient", "Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon" }); - api["EQUIPMENT_SLOT"] = LuaUtil::makeReadOnly(lua->sol().create_table_with( - "Helmet", MWWorld::InventoryStore::Slot_Helmet, - "Cuirass", MWWorld::InventoryStore::Slot_Cuirass, - "Greaves", MWWorld::InventoryStore::Slot_Greaves, - "LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron, - "RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron, - "LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet, - "RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet, - "Boots", MWWorld::InventoryStore::Slot_Boots, - "Shirt", MWWorld::InventoryStore::Slot_Shirt, - "Pants", MWWorld::InventoryStore::Slot_Pants, - "Skirt", MWWorld::InventoryStore::Slot_Skirt, - "Robe", MWWorld::InventoryStore::Slot_Robe, - "LeftRing", MWWorld::InventoryStore::Slot_LeftRing, - "RightRing", MWWorld::InventoryStore::Slot_RightRing, - "Amulet", MWWorld::InventoryStore::Slot_Amulet, - "Belt", MWWorld::InventoryStore::Slot_Belt, - "CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight, - "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft, - "Ammunition", MWWorld::InventoryStore::Slot_Ammunition - )); + api["EQUIPMENT_SLOT"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"Helmet", MWWorld::InventoryStore::Slot_Helmet}, + {"Cuirass", MWWorld::InventoryStore::Slot_Cuirass}, + {"Greaves", MWWorld::InventoryStore::Slot_Greaves}, + {"LeftPauldron", MWWorld::InventoryStore::Slot_LeftPauldron}, + {"RightPauldron", MWWorld::InventoryStore::Slot_RightPauldron}, + {"LeftGauntlet", MWWorld::InventoryStore::Slot_LeftGauntlet}, + {"RightGauntlet", MWWorld::InventoryStore::Slot_RightGauntlet}, + {"Boots", MWWorld::InventoryStore::Slot_Boots}, + {"Shirt", MWWorld::InventoryStore::Slot_Shirt}, + {"Pants", MWWorld::InventoryStore::Slot_Pants}, + {"Skirt", MWWorld::InventoryStore::Slot_Skirt}, + {"Robe", MWWorld::InventoryStore::Slot_Robe}, + {"LeftRing", MWWorld::InventoryStore::Slot_LeftRing}, + {"RightRing", MWWorld::InventoryStore::Slot_RightRing}, + {"Amulet", MWWorld::InventoryStore::Slot_Amulet}, + {"Belt", MWWorld::InventoryStore::Slot_Belt}, + {"CarriedRight", MWWorld::InventoryStore::Slot_CarriedRight}, + {"CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft}, + {"Ammunition", MWWorld::InventoryStore::Slot_Ammunition} + })); return LuaUtil::makeReadOnly(api); } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index d1c62e43e3..b021354a75 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -48,7 +48,7 @@ namespace MWLua struct AsyncPackageId { LuaUtil::ScriptsContainer* mContainer; - std::string mScript; + int mScriptId; sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); @@ -58,6 +58,7 @@ namespace MWLua // Implemented in uibindings.cpp sol::table initUserInterfacePackage(const Context&); + void clearUserInterface(); // Implemented in inputbindings.cpp sol::table initInputPackage(const Context&); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 38055c99b7..2f3df00f7f 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -7,11 +7,11 @@ #include #include -#include #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "luabindings.hpp" @@ -20,10 +20,9 @@ namespace MWLua { - LuaManager::LuaManager(const VFS::Manager* vfs, const std::vector& scriptLists) : mLua(vfs) + LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration) { Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion(); - mGlobalScriptList = LuaUtil::parseOMWScriptsFiles(vfs, scriptLists); mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry()); mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry()); @@ -33,6 +32,14 @@ namespace MWLua mGlobalScripts.setSerializer(mGlobalSerializer.get()); } + void LuaManager::initConfiguration() + { + mConfiguration.init(MWBase::Environment::get().getWorld()->getStore().getLuaScriptsCfg()); + Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):"; + for (size_t i = 0; i < mConfiguration.size(); ++i) + Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]); + } + void LuaManager::init() { Context context; @@ -67,51 +74,34 @@ namespace MWLua mLocalSettingsPackage = initLocalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); - mInputEvents.clear(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script started: " << path; + initConfiguration(); mInitialized = true; } - void Callback::operator()(sol::object arg) const - { - if (mHiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY] != sol::nil) - LuaUtil::call(mFunc, std::move(arg)); - else - { - Log(Debug::Debug) << "Ignored callback to removed script " << mHiddenData.get(SCRIPT_NAME_KEY); - } - } - void LuaManager::update(bool paused, float dt) { + if (mPlayer.isEmpty()) + return; // The game is not started yet. + ObjectRegistry* objectRegistry = mWorldView.getObjectRegistry(); - if (!mPlayer.isEmpty()) + MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (!(getId(mPlayer) == getId(newPlayerPtr))) + throw std::logic_error("Player Refnum was changed unexpectedly"); + if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) { - MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - if (!(getId(mPlayer) == getId(newPlayerPtr))) - throw std::logic_error("Player Refnum was changed unexpectedly"); - if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) - { - mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry - objectRegistry->registerPtr(mPlayer); - } + mPlayer = newPlayerPtr; // player was moved to another cell, update ptr in registry + objectRegistry->registerPtr(mPlayer); } - mWorldView.update(); - if (paused) - { - mInputEvents.clear(); - return; - } + mWorldView.update(); std::vector globalEvents = std::move(mGlobalEvents); std::vector localEvents = std::move(mLocalEvents); mGlobalEvents = std::vector(); mLocalEvents = std::vector(); + if (!paused) { // Update time and process timers double seconds = mWorldView.getGameTimeInSeconds() + dt; mWorldView.setGameTimeInSeconds(seconds); @@ -142,14 +132,6 @@ namespace MWLua mQueuedCallbacks.clear(); // Engine handlers in local scripts - PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); - if (playerScripts) - { - for (const auto& event : mInputEvents) - playerScripts->processInputEvent(event); - } - mInputEvents.clear(); - for (const LocalEngineEvent& e : mLocalEngineEvents) { LObject obj(e.mDest, objectRegistry); @@ -160,12 +142,15 @@ namespace MWLua } LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) - scripts->receiveEngineEvent(e.mEvent, objectRegistry); + scripts->receiveEngineEvent(e.mEvent); } mLocalEngineEvents.clear(); - for (LocalScripts* scripts : mActiveLocalScripts) - scripts->update(dt); + if (!paused) + { + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(dt); + } // Engine handlers in global scripts if (mPlayerChanged) @@ -173,27 +158,46 @@ namespace MWLua mPlayerChanged = false; mGlobalScripts.playerAdded(GObject(getId(mPlayer), objectRegistry)); } + if (mNewGameStarted) + { + mNewGameStarted = false; + mGlobalScripts.newGameStarted(); + } for (ObjectId id : mActorAddedEvents) mGlobalScripts.actorActive(GObject(id, objectRegistry)); mActorAddedEvents.clear(); - mGlobalScripts.update(dt); + if (!paused) + mGlobalScripts.update(dt); } - void LuaManager::applyQueuedChanges() + void LuaManager::synchronizedUpdate(bool paused, float dt) { + if (mPlayer.isEmpty()) + return; // The game is not started yet. + + // We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency. + PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (playerScripts && !paused) + { + for (const auto& event : mInputEvents) + playerScripts->processInputEvent(event); + playerScripts->inputUpdate(dt); + } + mInputEvents.clear(); + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); for (const std::string& message : mUIMessages) windowManager->messageBox(message); mUIMessages.clear(); for (std::unique_ptr& action : mActionQueue) - action->apply(mWorldView); + action->safeApply(mWorldView); mActionQueue.clear(); if (mTeleportPlayerAction) - mTeleportPlayerAction->apply(mWorldView); + mTeleportPlayerAction->safeApply(mWorldView); mTeleportPlayerAction.reset(); } @@ -205,14 +209,18 @@ namespace MWLua mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); + mNewGameStarted = false; mPlayerChanged = false; mWorldView.clear(); + mGlobalScripts.removeAllScripts(); + mGlobalScriptsStarted = false; if (!mPlayer.isEmpty()) { mPlayer.getCellRef().unsetRefNum(); mPlayer.getRefData().setLuaScripts(nullptr); mPlayer = MWWorld::Ptr(); } + clearUserInterface(); } void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) @@ -225,17 +233,38 @@ namespace MWLua mPlayer = ptr; LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, ESM::LuaScriptCfg::sPlayer); mActiveLocalScripts.insert(localScripts); mLocalEngineEvents.push_back({getId(ptr), LocalScripts::OnActive{}}); mPlayerChanged = true; } + void LuaManager::newGameStarted() + { + mNewGameStarted = true; + mInputEvents.clear(); + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + + void LuaManager::gameLoaded() + { + if (!mGlobalScriptsStarted) + mGlobalScripts.addAutoStartedScripts(); + mGlobalScriptsStarted = true; + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mWorldView.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts) + { + ESM::LuaScriptCfg::Flags flag = getLuaScriptFlag(ptr); + if (!mConfiguration.getListByFlag(flag).empty()) + localScripts = createLocalScripts(ptr, flag); // TODO: put to a queue and apply on next `update()` + } if (localScripts) { mActiveLocalScripts.insert(localScripts); @@ -281,26 +310,26 @@ namespace MWLua return localScripts->getActorControls(); } - void LuaManager::addLocalScript(const MWWorld::Ptr& ptr, const std::string& scriptPath) + void LuaManager::addCustomLocalScript(const MWWorld::Ptr& ptr, int scriptId) { LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts) { - localScripts = createLocalScripts(ptr); + localScripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); if (ptr.isInCell() && MWBase::Environment::get().getWorld()->isCellActive(ptr.getCell())) mActiveLocalScripts.insert(localScripts); } - localScripts->addNewScript(scriptPath); + localScripts->addCustomScript(scriptId); } - LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) + LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags flag) { assert(mInitialized); + assert(flag != ESM::LuaScriptCfg::sGlobal); std::shared_ptr scripts; - // When loading a game, it can be called before LuaManager::setPlayer, - // so we can't just check ptr == mPlayer here. - if (ptr.getCellRef().getRefIdRef() == "player") + if (flag == ESM::LuaScriptCfg::sPlayer) { + assert(ptr.getCellRef().getRefIdRef() == "player"); scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); @@ -309,11 +338,12 @@ namespace MWLua } else { - scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); + scripts->addAutoStartedScripts(); MWWorld::RefData& refData = ptr.getRefData(); refData.setLuaScripts(std::move(scripts)); @@ -344,8 +374,9 @@ namespace MWLua loadEvents(mLua.sol(), reader, mGlobalEvents, mLocalEvents, mContentFileMapping, mGlobalLoader.get()); mGlobalScripts.setSerializer(mGlobalLoader.get()); - mGlobalScripts.load(globalScripts, false); + mGlobalScripts.load(globalScripts); mGlobalScripts.setSerializer(mGlobalSerializer.get()); + mGlobalScriptsStarted = true; } void LuaManager::saveLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScripts& data) @@ -366,10 +397,10 @@ namespace MWLua } mWorldView.getObjectRegistry()->registerPtr(ptr); - LocalScripts* scripts = createLocalScripts(ptr); + LocalScripts* scripts = createLocalScripts(ptr, getLuaScriptFlag(ptr)); scripts->setSerializer(mLocalLoader.get()); - scripts->load(data, true); + scripts->load(data); scripts->setSerializer(mLocalSerializer.get()); // LiveCellRef is usually copied after loading, so this Ptr will become invalid and should be deregistered. @@ -380,15 +411,12 @@ namespace MWLua { Log(Debug::Info) << "Reload Lua"; mLua.dropScriptCache(); + initConfiguration(); { // Reload global scripts ESM::LuaScripts data; mGlobalScripts.save(data); - mGlobalScripts.removeAllScripts(); - for (const std::string& path : mGlobalScriptList) - if (mGlobalScripts.addNewScript(path)) - Log(Debug::Info) << "Global script restarted: " << path; - mGlobalScripts.load(data, false); + mGlobalScripts.load(data); } for (const auto& [id, ptr] : mWorldView.getObjectRegistry()->mObjectMapping) @@ -398,8 +426,11 @@ namespace MWLua continue; ESM::LuaScripts data; scripts->save(data); - scripts->load(data, true); + clearUserInterface(); + scripts->load(data); } + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->receiveEngineEvent(LocalScripts::OnActive()); } } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 91f48171f3..56d20c2720 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -19,25 +19,12 @@ namespace MWLua { - // Wrapper for a single-argument Lua function. - // Holds information about the script the function belongs to. - // Needed to prevent callback calls if the script was removed. - struct Callback - { - static constexpr std::string_view SCRIPT_NAME_KEY = "name"; - - sol::function mFunc; - sol::table mHiddenData; - - void operator()(sol::object arg) const; - }; - class LuaManager : public MWBase::LuaManager { public: - LuaManager(const VFS::Manager* vfs, const std::vector& globalScriptLists); + LuaManager(const VFS::Manager* vfs); - // Called by engine.cpp when environment is fully initialized. + // Called by engine.cpp when the environment is fully initialized. void init(); // Called by engine.cpp every frame. For performance reasons it works in a separate @@ -45,11 +32,12 @@ namespace MWLua void update(bool paused, float dt); // Called by engine.cpp from the main thread. Can use scene graph. - void applyQueuedChanges(); + void synchronizedUpdate(bool paused, float dt); // Available everywhere through the MWBase::LuaManager interface. // LuaManager queues these events and propagates to scripts on the next `update` call. - void newGameStarted() override { mGlobalScripts.newGameStarted(); } + void newGameStarted() override; + void gameLoaded() override; void objectAddedToScene(const MWWorld::Ptr& ptr) override; void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void registerObject(const MWWorld::Ptr& ptr) override; @@ -62,8 +50,8 @@ namespace MWLua void clear() override; // should be called before loading game or starting a new game to reset internal state. void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". - // Used only in luabindings - void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + // Used only in Lua bindings + void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } @@ -81,21 +69,27 @@ namespace MWLua void reloadAllScripts() override; // Used to call Lua callbacks from C++ - void queueCallback(Callback callback, sol::object arg) { mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); } + void queueCallback(LuaUtil::Callback callback, sol::object arg) + { + mQueuedCallbacks.push_back({std::move(callback), std::move(arg)}); + } // Wraps Lua callback into an std::function. // NOTE: Resulted function is not thread safe. Can not be used while LuaManager::update() or // any other Lua-related function is running. template - std::function wrapLuaCallback(const Callback& c) + std::function wrapLuaCallback(const LuaUtil::Callback& c) { return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; } private: - LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + void initConfiguration(); + LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, ESM::LuaScriptCfg::Flags); bool mInitialized = false; + bool mGlobalScriptsStarted = false; + LuaUtil::ScriptsConfiguration mConfiguration; LuaUtil::LuaState mLua; sol::table mNearbyPackage; sol::table mUserInterfacePackage; @@ -104,12 +98,12 @@ namespace MWLua sol::table mLocalSettingsPackage; sol::table mPlayerSettingsPackage; - std::vector mGlobalScriptList; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; WorldView mWorldView; bool mPlayerChanged = false; + bool mNewGameStarted = false; MWWorld::Ptr mPlayer; GlobalEventQueue mGlobalEvents; @@ -127,7 +121,7 @@ namespace MWLua struct CallbackWithData { - Callback mCallback; + LuaUtil::Callback mCallback; sol::object mArg; }; std::vector mQueuedCallbacks; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index 011d0ae9f3..624cf69da6 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -47,14 +47,15 @@ namespace MWLua return LObject(getId(r.mHitObject), worldView->getObjectRegistry()); }); - api["COLLISION_TYPE"] = LuaUtil::makeReadOnly(context.mLua->sol().create_table_with( - "World", MWPhysics::CollisionType_World, - "Door", MWPhysics::CollisionType_Door, - "Actor", MWPhysics::CollisionType_Actor, - "HeightMap", MWPhysics::CollisionType_HeightMap, - "Projectile", MWPhysics::CollisionType_Projectile, - "Water", MWPhysics::CollisionType_Water, - "Default", MWPhysics::CollisionType_Default)); + api["COLLISION_TYPE"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"World", MWPhysics::CollisionType_World}, + {"Door", MWPhysics::CollisionType_Door}, + {"Actor", MWPhysics::CollisionType_Actor}, + {"HeightMap", MWPhysics::CollisionType_HeightMap}, + {"Projectile", MWPhysics::CollisionType_Projectile}, + {"Water", MWPhysics::CollisionType_Water}, + {"Default", MWPhysics::CollisionType_Default} + })); api["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) { diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index d94ce1f49d..69206e8c37 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -1,19 +1,6 @@ #include "object.hpp" -#include "../mwclass/activator.hpp" -#include "../mwclass/armor.hpp" -#include "../mwclass/book.hpp" -#include "../mwclass/clothing.hpp" -#include "../mwclass/container.hpp" -#include "../mwclass/creature.hpp" -#include "../mwclass/door.hpp" -#include "../mwclass/ingredient.hpp" -#include "../mwclass/light.hpp" -#include "../mwclass/misc.hpp" -#include "../mwclass/npc.hpp" -#include "../mwclass/potion.hpp" -#include "../mwclass/static.hpp" -#include "../mwclass/weapon.hpp" +#include namespace MWLua { @@ -23,28 +10,34 @@ namespace MWLua return std::to_string(id.mIndex) + "_" + std::to_string(id.mContentFile); } - const static std::map classNames = { - {typeid(MWClass::Activator), "Activator"}, - {typeid(MWClass::Armor), "Armor"}, - {typeid(MWClass::Book), "Book"}, - {typeid(MWClass::Clothing), "Clothing"}, - {typeid(MWClass::Container), "Container"}, - {typeid(MWClass::Creature), "Creature"}, - {typeid(MWClass::Door), "Door"}, - {typeid(MWClass::Ingredient), "Ingredient"}, - {typeid(MWClass::Light), "Light"}, - {typeid(MWClass::Miscellaneous), "Miscellaneous"}, - {typeid(MWClass::Npc), "NPC"}, - {typeid(MWClass::Potion), "Potion"}, - {typeid(MWClass::Static), "Static"}, - {typeid(MWClass::Weapon), "Weapon"}, + struct LuaObjectTypeInfo + { + std::string_view mName; + ESM::LuaScriptCfg::Flags mFlag = 0; + }; + + const static std::unordered_map luaObjectTypeInfo = { + {ESM::REC_ACTI, {"Activator", ESM::LuaScriptCfg::sActivator}}, + {ESM::REC_ARMO, {"Armor", ESM::LuaScriptCfg::sArmor}}, + {ESM::REC_BOOK, {"Book", ESM::LuaScriptCfg::sBook}}, + {ESM::REC_CLOT, {"Clothing", ESM::LuaScriptCfg::sClothing}}, + {ESM::REC_CONT, {"Container", ESM::LuaScriptCfg::sContainer}}, + {ESM::REC_CREA, {"Creature", ESM::LuaScriptCfg::sCreature}}, + {ESM::REC_DOOR, {"Door", ESM::LuaScriptCfg::sDoor}}, + {ESM::REC_INGR, {"Ingredient", ESM::LuaScriptCfg::sIngredient}}, + {ESM::REC_LIGH, {"Light", ESM::LuaScriptCfg::sLight}}, + {ESM::REC_MISC, {"Miscellaneous", ESM::LuaScriptCfg::sMiscItem}}, + {ESM::REC_NPC_, {"NPC", ESM::LuaScriptCfg::sNPC}}, + {ESM::REC_ALCH, {"Potion", ESM::LuaScriptCfg::sPotion}}, + {ESM::REC_STAT, {"Static"}}, + {ESM::REC_WEAP, {"Weapon", ESM::LuaScriptCfg::sWeapon}}, }; - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback) + std::string_view getLuaObjectTypeName(ESM::RecNameInts type, std::string_view fallback) { - auto it = classNames.find(cls_type); - if (it != classNames.end()) - return it->second; + auto it = luaObjectTypeInfo.find(type); + if (it != luaObjectTypeInfo.end()) + return it->second.mName; else return fallback; } @@ -55,13 +48,31 @@ namespace MWLua return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; } - std::string_view getMWClassName(const MWWorld::Ptr& ptr) + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr) { + // Behaviour of this function is a part of OpenMW Lua API. We can not just return + // `ptr.getTypeDescription()` because its implementation is distributed over many files + // and can be accidentally changed. We use `ptr.getTypeDescription()` only as a fallback + // for types that are not present in `luaObjectTypeInfo` (for such types result stability + // is not necessary because they are not listed in OpenMW Lua documentation). if (ptr.getCellRef().getRefIdRef() == "player") return "Player"; if (isMarker(ptr)) return "Marker"; - return getMWClassName(typeid(ptr.getClass()), ptr.getTypeName()); + return getLuaObjectTypeName(static_cast(ptr.getType()), /*fallback=*/ptr.getTypeDescription()); + } + + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr) + { + if (ptr.getCellRef().getRefIdRef() == "player") + return ESM::LuaScriptCfg::sPlayer; + if (isMarker(ptr)) + return 0; + auto it = luaObjectTypeInfo.find(static_cast(ptr.getType())); + if (it != luaObjectTypeInfo.end()) + return it->second.mFlag; + else + return 0; } std::string ptrToString(const MWWorld::Ptr& ptr) @@ -69,7 +80,7 @@ namespace MWLua std::string res = "object"; res.append(idToString(getId(ptr))); res.append(" ("); - res.append(getMWClassName(ptr)); + res.append(getLuaObjectTypeName(ptr)); res.append(", "); res.append(ptr.getCellRef().getRefIdRef()); res.append(")"); diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp index c0b6bf1919..5b1b5df74e 100644 --- a/apps/openmw/mwlua/object.hpp +++ b/apps/openmw/mwlua/object.hpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -19,8 +21,12 @@ namespace MWLua std::string idToString(const ObjectId& id); std::string ptrToString(const MWWorld::Ptr& ptr); bool isMarker(const MWWorld::Ptr& ptr); - std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown"); - std::string_view getMWClassName(const MWWorld::Ptr& ptr); + std::string_view getLuaObjectTypeName(ESM::RecNameInts recordType, std::string_view fallback = "Unknown"); + std::string_view getLuaObjectTypeName(const MWWorld::Ptr& ptr); + + // Each script has a set of flags that controls to which objects the script should be + // automatically attached. This function maps each object types to one of the flags. + ESM::LuaScriptCfg::Flags getLuaScriptFlag(const MWWorld::Ptr& ptr); // Holds a mapping ObjectId -> MWWord::Ptr. class ObjectRegistry @@ -64,7 +70,7 @@ namespace MWLua ObjectId id() const { return mId; } std::string toString() const; - std::string_view type() const { return getMWClassName(ptr()); } + std::string_view type() const { return getLuaObjectTypeName(ptr()); } // Updates and returns the underlying Ptr. Throws an exception if object is not available. const MWWorld::Ptr& ptr() const; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index b7607c8b2c..9857170b32 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -42,13 +42,12 @@ namespace MWLua template using Cell = std::conditional_t, LCell, GCell>; - template - static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr) + static const MWWorld::Ptr& requireRecord(ESM::RecNameInts recordType, const MWWorld::Ptr& ptr) { - if (typeid(Class) != typeid(ptr.getClass())) + if (ptr.getType() != recordType) { std::string msg = "Requires type '"; - msg.append(getMWClassName(typeid(Class))); + msg.append(getLuaObjectTypeName(recordType)); msg.append("', but applied to "); msg.append(ptrToString(ptr)); throw std::runtime_error(msg); @@ -141,21 +140,55 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) + objectT["addScript"] = [lua=context.mLua, luaManager=context.mLuaManager](const GObject& object, std::string_view path) { - luaManager->addLocalScript(object.ptr(), path); + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) + throw std::runtime_error("Script without CUSTOM tag can not be added dynamically: " + std::string(path)); + luaManager->addCustomLocalScript(object.ptr(), *scriptId); + }; + objectT["hasScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + return false; + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (localScripts) + return localScripts->hasScript(*scriptId); + else + return false; + }; + objectT["removeScript"] = [lua=context.mLua](const GObject& object, std::string_view path) + { + const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); + std::optional scriptId = cfg.findId(path); + if (!scriptId) + throw std::runtime_error("Unknown script: " + std::string(path)); + MWWorld::Ptr ptr = object.ptr(); + LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); + if (!localScripts || !localScripts->hasScript(*scriptId)) + throw std::runtime_error("There is no script " + std::string(path) + " on " + ptrToString(ptr)); + ESM::LuaScriptCfg::Flags flags = cfg[*scriptId].mFlags; + if ((flags & (localScripts->getAutoStartMode() | ESM::LuaScriptCfg::sCustom)) != ESM::LuaScriptCfg::sCustom) + throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); + localScripts->removeScript(*scriptId); }; - objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, - const osg::Vec3f& pos, const sol::optional& optRot) + objectT["teleport"] = [context](const GObject& object, std::string_view cell, + const osg::Vec3f& pos, const sol::optional& optRot) { MWWorld::Ptr ptr = object.ptr(); osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3(); - auto action = std::make_unique(object.id(), std::string(cell), pos, rot); + auto action = std::make_unique(context.mLua, object.id(), std::string(cell), pos, rot); if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) - luaManager->addTeleportPlayerAction(std::move(action)); + context.mLuaManager->addTeleportPlayerAction(std::move(action)); else - luaManager->addAction(std::move(action)); + context.mLuaManager->addAction(std::move(action)); }; } else @@ -189,7 +222,7 @@ namespace MWLua template static void addDoorBindings(sol::usertype& objectT, const Context& context) { - auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass(o.ptr()); }; + auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireRecord(ESM::REC_DOOR, o.ptr()); }; objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o) { @@ -319,7 +352,7 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment) + objectT["setEquipment"] = [context](const GObject& obj, sol::table equipment) { if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) { @@ -327,7 +360,8 @@ namespace MWLua throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); return; } - manager->addAction(std::make_unique(obj.id(), parseEquipmentTable(equipment))); + context.mLuaManager->addAction(std::make_unique( + context.mLua, obj.id(), parseEquipmentTable(equipment))); }; // TODO diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index ff0349b3c6..e8cdd120ac 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -13,11 +13,11 @@ namespace MWLua class PlayerScripts : public LocalScripts { public: - PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) + PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) { registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, - &mActionHandlers}); + &mActionHandlers, &mInputUpdateHandlers}); } void processInputEvent(const MWBase::LuaManager::InputEvent& event) @@ -43,12 +43,15 @@ namespace MWLua } } + void inputUpdate(float dt) { callEngineHandlers(mInputUpdateHandlers, dt); } + private: EngineHandlerList mKeyPressHandlers{"onKeyPress"}; EngineHandlerList mKeyReleaseHandlers{"onKeyRelease"}; EngineHandlerList mControllerButtonPressHandlers{"onControllerButtonPress"}; EngineHandlerList mControllerButtonReleaseHandlers{"onControllerButtonRelease"}; EngineHandlerList mActionHandlers{"onInputAction"}; + EngineHandlerList mInputUpdateHandlers{"onInputUpdate"}; }; } diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index 4fae84cd40..9982b857c8 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -1,18 +1,192 @@ -#include "luabindings.hpp" +#include +#include +#include +#include "context.hpp" +#include "actions.hpp" #include "luamanagerimp.hpp" namespace MWLua { + namespace + { + std::set allElements; + + class UiAction final : public Action + { + public: + enum Type + { + CREATE = 0, + UPDATE, + DESTROY, + }; + + UiAction(Type type, std::shared_ptr element, LuaUtil::LuaState* state) + : Action(state) + , mType{ type } + , mElement{ std::move(element) } + {} + + void apply(WorldView&) const override + { + try { + switch (mType) + { + case CREATE: + mElement->create(); + break; + case UPDATE: + mElement->update(); + break; + case DESTROY: + mElement->destroy(); + break; + } + } + catch (std::exception& e) + { + // prevent any actions on a potentially corrupted widget + mElement->mRoot = nullptr; + throw; + } + } + + std::string toString() const override + { + std::string result; + switch (mType) + { + case CREATE: + result += "Create"; + break; + case UPDATE: + result += "Update"; + break; + case DESTROY: + result += "Destroy"; + break; + } + result += " UI"; + return result; + } + + private: + Type mType; + std::shared_ptr mElement; + }; + + // Lua arrays index from 1 + inline size_t fromLuaIndex(size_t i) { return i - 1; } + inline size_t toLuaIndex(size_t i) { return i + 1; } + } sol::table initUserInterfacePackage(const Context& context) { - sol::table api(context.mLua->sol(), sol::create); + auto uiContent = context.mLua->sol().new_usertype("UiContent"); + uiContent[sol::meta_function::length] = [](const LuaUi::Content& content) + { + return content.size(); + }; + uiContent[sol::meta_function::index] = sol::overload( + [](const LuaUi::Content& content, size_t index) + { + return content.at(fromLuaIndex(index)); + }, + [](const LuaUi::Content& content, std::string_view name) + { + return content.at(name); + }); + uiContent[sol::meta_function::new_index] = sol::overload( + [](LuaUi::Content& content, size_t index, const sol::table& table) + { + content.assign(fromLuaIndex(index), table); + }, + [](LuaUi::Content& content, size_t index, sol::nil_t nil) + { + content.remove(fromLuaIndex(index)); + }, + [](LuaUi::Content& content, std::string_view name, const sol::table& table) + { + content.assign(name, table); + }, + [](LuaUi::Content& content, std::string_view name, sol::nil_t nil) + { + content.remove(name); + }); + uiContent["insert"] = [](LuaUi::Content& content, size_t index, const sol::table& table) + { + content.insert(fromLuaIndex(index), table); + }; + uiContent["add"] = [](LuaUi::Content& content, const sol::table& table) + { + content.insert(content.size(), table); + }; + uiContent["indexOf"] = [](LuaUi::Content& content, const sol::table& table) -> sol::optional + { + size_t index = content.indexOf(table); + if (index < content.size()) + return toLuaIndex(index); + else + return sol::nullopt; + }; + + auto element = context.mLua->sol().new_usertype("Element"); + element["layout"] = sol::property( + [](LuaUi::Element& element) + { + return element.mLayout; + }, + [](LuaUi::Element& element, const sol::table& layout) + { + element.mLayout = layout; + } + ); + element["update"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy || element->mUpdate) + return; + element->mUpdate = true; + context.mLuaManager->addAction(std::make_unique(UiAction::UPDATE, element, context.mLua)); + }; + element["destroy"] = [context](const std::shared_ptr& element) + { + if (element->mDestroy) + return; + allElements.erase(element.get()); + element->mDestroy = true; + context.mLuaManager->addAction(std::make_unique(UiAction::DESTROY, element, context.mLua)); + }; + + sol::table api = context.mLua->newTable(); api["showMessage"] = [luaManager=context.mLuaManager](std::string_view message) { luaManager->addUIMessage(message); }; + api["content"] = [](const sol::table& table) + { + return LuaUi::Content(table); + }; + api["create"] = [context](const sol::table& layout) + { + auto element = std::make_shared(layout); + allElements.emplace(element.get()); + context.mLuaManager->addAction(std::make_unique(UiAction::CREATE, element, context.mLua)); + return element; + }; + + sol::table typeTable = context.mLua->newTable(); + for (const auto& it : LuaUi::widgetTypeToName()) + typeTable.set(it.second, it.first); + api["TYPE"] = LuaUtil::makeReadOnly(typeTable); + return LuaUtil::makeReadOnly(api); } + void clearUserInterface() + { + for (auto element : allElements) + element->destroy(); + allElements.clear(); + } } diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 6ad6ef369e..d435bd6c44 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -1,5 +1,9 @@ #include "activespells.hpp" +#include + +#include + #include #include @@ -12,6 +16,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + #include "../mwworld/esmstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -94,6 +100,11 @@ namespace MWMechanics , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) {} + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) + : mId(params.mId), mDisplayName(params.mDisplayName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(params.mSlot), mType(params.mType), mWorsenings(-1) + {} + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { ESM::ActiveSpells::ActiveSpellParams params; @@ -218,10 +229,19 @@ namespace MWMechanics { const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? bool removedSpell = false; + std::optional reflected; for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); - if(remove) + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(result == MagicApplicationResult::REFLECTED) + { + if(!reflected) + reflected = {*spellIt, ptr}; + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if(result == MagicApplicationResult::REMOVED) it = spellIt->mEffects.erase(it); else ++it; @@ -229,12 +249,30 @@ namespace MWMechanics if(removedSpell) break; } + if(reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_Reflect"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if(animation && !reflectStatic->mModel.empty()) + animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } if(removedSpell) continue; bool remove = false; if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent) - remove = !spells.hasSpell(spellIt->mId); + { + try + { + remove = !spells.hasSpell(spellIt->mId); + } + catch(const std::runtime_error& e) + { + remove = true; + Log(Debug::Error) << "Removing active effect: " << e.what(); + } + } else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment) { const auto& store = ptr.getClass().getInventoryStore(ptr); diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 0538d7a8b7..524bdc0475 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -50,6 +50,8 @@ namespace MWMechanics ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor); + ESM::ActiveSpells::ActiveSpellParams toEsm() const; friend class ActiveSpells; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index bde18aa584..ce2465ec97 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -240,10 +240,7 @@ namespace MWMechanics { // magic effects adjustMagicEffects (ptr, duration); - if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) - calculateDynamicStats (ptr); - calculateCreatureStatModifiers (ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } @@ -446,6 +443,22 @@ namespace MWMechanics } } + void Actors::stopCombat(const MWWorld::Ptr& ptr) + { + auto& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector targets; + if(ai.getCombatTargets(targets)) + { + std::set allySet; + getActorsSidingWith(ptr, allySet); + std::vector allies(allySet.begin(), allySet.end()); + for(const auto& ally : allies) + ally.getClass().getCreatureStats(ally).getAiSequence().stopCombat(targets); + for(const auto& target : targets) + target.getClass().getCreatureStats(target).getAiSequence().stopCombat(allies); + } + } + void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer) { // No combat for totally static creatures @@ -654,29 +667,6 @@ namespace MWMechanics updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } - void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) - { - CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); - - float intelligence = creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified(); - - float base = 1.f; - if (ptr == getPlayer()) - base = MWBase::Environment::get().getWorld()->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); - else - base = MWBase::Environment::get().getWorld()->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); - - double magickaFactor = base + - creatureStats.getMagicEffects().get (EffectKey (ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; - - DynamicStat magicka = creatureStats.getMagicka(); - float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); - float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; - magicka.setModified(magicka.getModified() + diff, 0); - magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); - creatureStats.setMagicka(magicka); - } - void Actors::restoreDynamicStats (const MWWorld::Ptr& ptr, double hours, bool sleep) { MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats (ptr); @@ -771,14 +761,6 @@ namespace MWMechanics stats.setFatigue (fatigue); } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - - if (creatureStats.needToRecalcDynamicStats()) - calculateDynamicStats(ptr); - } - bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) { PtrActorMap::iterator it = mActors.find(ptr); @@ -879,7 +861,7 @@ namespace MWMechanics MWWorld::ContainerStoreIterator torch = inventoryStore.end(); for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) { - if (it->getTypeName() == typeid(ESM::Light).name() && + if (it->getType() == ESM::Light::sRecordId && it->getClass().canBeEquipped(*it, ptr).first) { torch = it; @@ -894,10 +876,10 @@ namespace MWMechanics if (!ptr.getClass().getCreatureStats (ptr).getAiSequence().isInCombat()) { // For non-hostile NPCs, unequip whatever is in the left slot in favor of a light. - if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name()) + if (heldIter != inventoryStore.end() && heldIter->getType() != ESM::Light::sRecordId) inventoryStore.unequipItem(*heldIter, ptr); } - else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name()) + else if (heldIter == inventoryStore.end() || heldIter->getType() == ESM::Light::sRecordId) { // For hostile NPCs, see if they have anything better to equip first auto shield = inventoryStore.getPreferredShield(ptr); @@ -916,7 +898,7 @@ namespace MWMechanics } else { - if (heldIter != inventoryStore.end() && heldIter->getTypeName() == typeid(ESM::Light).name()) + if (heldIter != inventoryStore.end() && heldIter->getType() == ESM::Light::sRecordId) { // At day, unequip lights and auto equip shields or other suitable items // (Note: autoEquip will ignore lights) @@ -1013,7 +995,7 @@ namespace MWMechanics // Calm witness down if (ptr.getClass().isClass(ptr, "Guard")) creatureStats.getAiSequence().stopPursuit(); - creatureStats.getAiSequence().stopCombat(); + stopCombat(ptr); // Reset factors to attack creatureStats.setAttacked(false); @@ -1047,13 +1029,10 @@ namespace MWMechanics void Actors::updateProcessingRange() { // We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876) - static const float maxProcessingRange = 7168.f; - static const float minProcessingRange = maxProcessingRange / 2.f; + static const float maxRange = 7168.f; + static const float minRange = maxRange / 2.f; - float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game"); - actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange); - actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange); - mActorsProcessingRange = actorsProcessingRange; + mActorsProcessingRange = std::clamp(Settings::Manager::getFloat("actors processing range", "Game"), minRange, maxRange); } void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) @@ -1349,7 +1328,7 @@ namespace MWMechanics angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); osg::Vec2f posAtT = relPos + relSpeed * t; float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * collisionDist * maxSpeed); - coef *= osg::clampBetween((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); + coef *= std::clamp((maxDistForPartialAvoiding - dist) / (maxDistForPartialAvoiding - maxDistForStrictAvoiding), 0.f, 1.f); movementCorrection = posAtT * coef; if (otherPtr.getClass().getCreatureStats(otherPtr).isDead()) // In case of dead body still try to go around (it looks natural), but reduce the correction twice. @@ -1530,15 +1509,13 @@ namespace MWMechanics stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true); } - if(iter->first.getClass().isNpc()) + if(inProcessingRange && iter->first.getClass().isNpc()) { // We can not update drowning state for actors outside of AI distance - they can not resurface to breathe - if (inProcessingRange) - updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - - if (timerUpdateEquippedLight == 0) - updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); + updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); } + if(timerUpdateEquippedLight == 0 && iter->first.getClass().hasInventoryStore(iter->first)) + updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); if (luaControls && isConscious(iter->first)) { @@ -1637,6 +1614,7 @@ namespace MWMechanics if (playerCharacter) { + MWBase::Environment::get().getWorld()->applyDeferredPreviewRotationToPlayer(duration); playerCharacter->update(duration); playerCharacter->setVisibility(1.f); } @@ -1708,13 +1686,9 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(iter->first, "hit"); // Apply soultrap - if (iter->first.getTypeName() == typeid(ESM::Creature).name()) + if (iter->first.getType() == ESM::Creature::sRecordId) soulTrap(iter->first); - // Magic effects will be reset later, and the magic effect that could kill the actor - // needs to be determined now - calculateCreatureStatModifiers(iter->first, 0); - if (cls.isEssential(iter->first)) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } @@ -1730,8 +1704,6 @@ namespace MWMechanics // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); - // Reset dynamic stats, attributes and skills - calculateCreatureStatModifiers(iter->first, 0); stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) @@ -1816,10 +1788,6 @@ namespace MWMechanics continue; adjustMagicEffects (iter->first, duration); - if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) - calculateDynamicStats (iter->first); - - calculateCreatureStatModifiers (iter->first, duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2015,7 +1983,7 @@ namespace MWMechanics if (stats.isDead()) continue; - // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package + // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Wander packages before the Follow/Escort package // Actors that are targeted by this actor's Follow or Escort packages also side with them for (const auto& package : stats.getAiSequence()) { @@ -2031,7 +1999,7 @@ namespace MWMechanics } break; } - else if (package->getTypeId() != AiPackageTypeId::Combat && package->getTypeId() != AiPackageTypeId::Wander) + else if (package->getTypeId() > AiPackageTypeId::Wander && package->getTypeId() <= AiPackageTypeId::Activate) // Don't count "fake" package types break; } } @@ -2209,7 +2177,6 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { adjustMagicEffects(ptr, 0.f); - calculateCreatureStatModifiers(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr &ptr) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 9950a591ab..f922be6556 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -43,10 +43,6 @@ namespace MWMechanics void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); - void calculateDynamicStats (const MWWorld::Ptr& ptr); - - void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateRestoration (const MWWorld::Ptr& ptr, float duration); void updateDrowning (const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer); @@ -115,6 +111,8 @@ namespace MWMechanics ///< This function is normally called automatically during the update process, but it can /// also be called explicitly at any time to force an update. + /// Removes an actor from combat and makes all of their allies stop fighting the actor's targets + void stopCombat(const MWWorld::Ptr& ptr); /** Start combat between two actors @Notes: If againstPlayer = true then actor2 should be the Player. If one of the combatants is creature it should be actor1. diff --git a/apps/openmw/mwmechanics/actorutil.cpp b/apps/openmw/mwmechanics/actorutil.cpp index 04cbb8e9f5..4a587dd662 100644 --- a/apps/openmw/mwmechanics/actorutil.cpp +++ b/apps/openmw/mwmechanics/actorutil.cpp @@ -29,4 +29,12 @@ namespace MWMechanics const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); return effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; } + + CreatureCustomDataResetter::CreatureCustomDataResetter(const MWWorld::Ptr& ptr) : mPtr(ptr) {} + + CreatureCustomDataResetter::~CreatureCustomDataResetter() + { + if(!mPtr.isEmpty()) + mPtr.getRefData().setCustomData({}); + } } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index a226fc9cb6..a66d8866bf 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -86,6 +86,14 @@ namespace MWMechanics template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount); template void modifyBaseInventory(const std::string& containerId, const std::string& itemId, int amount); + + struct CreatureCustomDataResetter + { + MWWorld::Ptr mPtr; + + CreatureCustomDataResetter(const MWWorld::Ptr& ptr); + ~CreatureCustomDataResetter(); + }; } #endif diff --git a/apps/openmw/mwmechanics/aiactivate.cpp b/apps/openmw/mwmechanics/aiactivate.cpp index b4ddf0c030..06aef0cdb0 100644 --- a/apps/openmw/mwmechanics/aiactivate.cpp +++ b/apps/openmw/mwmechanics/aiactivate.cpp @@ -13,8 +13,8 @@ namespace MWMechanics { - AiActivate::AiActivate(const std::string &objectId) - : mObjectId(objectId) + AiActivate::AiActivate(const std::string &objectId, bool repeat) + : TypedAiPackage(repeat), mObjectId(objectId) { } @@ -48,6 +48,7 @@ namespace MWMechanics { std::unique_ptr activate(new ESM::AiSequence::AiActivate()); activate->mTargetId = mObjectId; + activate->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Activate; @@ -56,7 +57,7 @@ namespace MWMechanics } AiActivate::AiActivate(const ESM::AiSequence::AiActivate *activate) - : mObjectId(activate->mTargetId) + : AiActivate(activate->mTargetId, activate->mRepeat) { } } diff --git a/apps/openmw/mwmechanics/aiactivate.hpp b/apps/openmw/mwmechanics/aiactivate.hpp index dc7e0bb26b..ad4d4e6064 100644 --- a/apps/openmw/mwmechanics/aiactivate.hpp +++ b/apps/openmw/mwmechanics/aiactivate.hpp @@ -24,7 +24,7 @@ namespace MWMechanics public: /// Constructor /** \param objectId Reference to object to activate **/ - explicit AiActivate(const std::string &objectId); + explicit AiActivate(const std::string &objectId, bool repeat); explicit AiActivate(const ESM::AiSequence::AiActivate* activate); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index eb139c918d..9eb8a00763 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include "../mwphysics/collisiontype.hpp" @@ -137,7 +137,10 @@ namespace MWMechanics } storage.updateCombatMove(duration); + storage.mRotateMove = false; if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); + if (storage.mRotateMove) + return false; storage.updateAttack(characterController); } else @@ -277,7 +280,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 auto navigator = world->getNavigator(); - const auto hit = navigator->raycast(halfExtents, vActorPos, vTargetPos, navigatorFlags); + const auto hit = DetourNavigator::raycast(*navigator, halfExtents, vActorPos, vTargetPos, navigatorFlags); if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { @@ -442,7 +445,7 @@ namespace MWMechanics storage.mCurrentAction->getCombatRange(isRangedCombat); float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f); float targetAngleRadians = storage.mMovement.mRotation[axis]; - smoothTurn(actor, targetAngleRadians, axis, eps); + storage.mRotateMove = !smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 5425f1af0b..34e726412c 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -33,6 +33,7 @@ namespace MWMechanics bool mAttack; float mAttackRange; bool mCombatMove; + bool mRotateMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; std::shared_ptr mCurrentAction; @@ -65,6 +66,7 @@ namespace MWMechanics mAttack(false), mAttackRange(0.0f), mCombatMove(false), + mRotateMove(false), mLastTargetPos(0,0,0), mCell(nullptr), mCurrentAction(), diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 75c0461105..0184c6e66f 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -20,16 +20,16 @@ namespace MWMechanics { - AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { mTargetActorRefId = actorId; } - AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z) - : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z, bool repeat) + : TypedAiPackage(repeat), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { @@ -37,11 +37,8 @@ namespace MWMechanics } AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) - : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(escort->mRemainingDuration > 0) + : TypedAiPackage(escort->mRepeat), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) + , mDuration(escort->mData.mDuration) , mRemainingDuration(escort->mRemainingDuration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) @@ -103,10 +100,12 @@ namespace MWMechanics escort->mData.mX = mX; escort->mData.mY = mY; escort->mData.mZ = mZ; + escort->mData.mDuration = mDuration; escort->mTargetId = mTargetActorRefId; escort->mTargetActorId = mTargetActorId; escort->mRemainingDuration = mRemainingDuration; escort->mCellId = mCellId; + escort->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Escort; diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 27a177893d..c49e227e09 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -22,11 +22,11 @@ namespace MWMechanics /// Implementation of AiEscort /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time \implement AiEscort **/ - AiEscort(const std::string &actorId, int duration, float x, float y, float z); + AiEscort(const std::string &actorId, int duration, float x, float y, float z, bool repeat); /// Implementation of AiEscortCell /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time \implement AiEscortCell **/ - AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z); + AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z, bool repeat); AiEscort(const ESM::AiSequence::AiEscort* escort); diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index ec23679977..43a0f25c62 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -28,36 +28,20 @@ namespace MWMechanics { int AiFollow::mFollowIndexCounter = 0; -AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z, bool repeat) +: TypedAiPackage(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) , mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { mTargetActorRefId = actorId; } -AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z, bool repeat) +: TypedAiPackage(repeat), mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) , mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) { mTargetActorRefId = actorId; } -AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} - -AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z) -: mAlwaysFollow(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) -{ - mTargetActorRefId = actor.getCellRef().getRefId(); - mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); -} - AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!commanded)) , mAlwaysFollow(true), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0) @@ -68,12 +52,9 @@ AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) } AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) - : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded)) + : TypedAiPackage(makeDefaultOptions().withShouldCancelPreviousAi(!follow->mCommanded).withRepeat(follow->mRepeat)) , mAlwaysFollow(follow->mAlwaysFollow) - // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. - // The exact value of mDuration only matters for repeating packages. - // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. - , mDuration(follow->mRemainingDuration) + , mDuration(follow->mData.mDuration) , mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) @@ -160,12 +141,15 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte if (actor.getCell()->isExterior()) //Outside? { if (mCellId == "") //No cell to travel to + { + mRemainingDuration = mDuration; return true; + } } - else + else if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to { - if (mCellId == actor.getCell()->getCell()->mName) //Cell to travel to - return true; + mRemainingDuration = mDuration; + return true; } } } @@ -221,6 +205,7 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; + follow->mData.mDuration = mDuration; follow->mTargetId = mTargetActorRefId; follow->mTargetActorId = mTargetActorId; follow->mRemainingDuration = mRemainingDuration; @@ -228,6 +213,7 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const follow->mAlwaysFollow = mAlwaysFollow; follow->mCommanded = isCommanded(); follow->mActive = mActive; + follow->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Follow; diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index c4ac5eb3f2..b83e464fa1 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -40,12 +40,10 @@ namespace MWMechanics class AiFollow final : public TypedAiPackage { public: - AiFollow(const std::string &actorId, float duration, float x, float y, float z); - AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z); /// Follow Actor for duration or until you arrive at a world position - AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z); + AiFollow(const std::string &actorId, float duration, float x, float y, float z, bool repeat); /// Follow Actor for duration or until you arrive at a position in a cell - AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z); + AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z, bool repeat); /// Follow Actor indefinitively AiFollow(const MWWorld::Ptr& actor, bool commanded=false); diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 5270b74166..498c6e3050 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -108,7 +108,7 @@ namespace MWMechanics /// Upon adding this Ai package, should the Ai Sequence attempt to cancel previous Ai packages (default true)? bool shouldCancelPreviousAi() const { return mOptions.mShouldCancelPreviousAi; } - /// Return true if this package should repeat. Currently only used for Wander packages. + /// Return true if this package should repeat. bool getRepeat() const { return mOptions.mRepeat; } virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index bf4cf28deb..545ec7f140 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -31,14 +31,13 @@ void AiSequence::copy (const AiSequence& sequence) sequence.mAiState.copy(mAiState); } -AiSequence::AiSequence() : mDone (false), mRepeat(false), mLastAiPackage(AiPackageTypeId::None) {} +AiSequence::AiSequence() : mDone (false), mLastAiPackage(AiPackageTypeId::None) {} AiSequence::AiSequence (const AiSequence& sequence) { copy (sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; - mRepeat = sequence.mRepeat; } AiSequence& AiSequence::operator= (const AiSequence& sequence) @@ -159,6 +158,7 @@ bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const return false; } +// TODO: use std::list::remove_if for all these methods when we switch to C++20 void AiSequence::stopCombat() { for(auto it = mPackages.begin(); it != mPackages.end(); ) @@ -172,6 +172,19 @@ void AiSequence::stopCombat() } } +void AiSequence::stopCombat(const std::vector& targets) +{ + for(auto it = mPackages.begin(); it != mPackages.end(); ) + { + if ((*it)->getTypeId() == AiPackageTypeId::Combat && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end()) + { + it = mPackages.erase(it); + } + else + ++it; + } +} + void AiSequence::stopPursuit() { for(auto it = mPackages.begin(); it != mPackages.end(); ) @@ -281,7 +294,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac if (package->execute(actor, characterController, mAiState, duration)) { // Put repeating noncombat AI packages on the end of the stack so they can be used again - if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) + if (isActualAiPackage(packageTypeId) && package->getRepeat()) { package->reset(); mPackages.push_back(package->clone()); @@ -355,7 +368,6 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo else ++it; } - mRepeat=false; } // insert new package in correct place depending on priority @@ -401,10 +413,6 @@ const AiPackage& MWMechanics::AiSequence::getActivePackage() void AiSequence::fill(const ESM::AIPackageList &list) { - // If there is more than one package in the list, enable repeating - if (list.mList.size() >= 2) - mRepeat = true; - for (const auto& esmPackage : list.mList) { std::unique_ptr package; @@ -420,22 +428,22 @@ void AiSequence::fill(const ESM::AIPackageList &list) else if (esmPackage.mType == ESM::AI_Escort) { ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); + package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Travel) { ESM::AITravel data = esmPackage.mTravel; - package = std::make_unique(data.mX, data.mY, data.mZ); + package = std::make_unique(data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } else if (esmPackage.mType == ESM::AI_Activate) { ESM::AIActivate data = esmPackage.mActivate; - package = std::make_unique(data.mName.toString()); + package = std::make_unique(data.mName.toString(), data.mShouldRepeat != 0); } else //if (esmPackage.mType == ESM::AI_Follow) { ESM::AITarget data = esmPackage.mTarget; - package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ); + package = std::make_unique(data.mId.toString(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } mPackages.push_back(std::move(package)); } @@ -454,24 +462,6 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) if (!sequence.mPackages.empty()) clear(); - // If there is more than one non-combat, non-pursue package in the list, enable repeating. - int count = 0; - for (auto& container : sequence.mPackages) - { - switch (container.mType) - { - case ESM::AiSequence::Ai_Wander: - case ESM::AiSequence::Ai_Travel: - case ESM::AiSequence::Ai_Escort: - case ESM::AiSequence::Ai_Follow: - case ESM::AiSequence::Ai_Activate: - ++count; - } - } - - if (count > 1) - mRepeat = true; - // Load packages for (auto& container : sequence.mPackages) { diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 645524d381..77f6b2f7c0 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -43,9 +43,6 @@ namespace MWMechanics ///Finished with top AIPackage, set for one frame bool mDone; - ///Does this AI sequence repeat (repeating of Wander packages handled separately) - bool mRepeat; - ///Copy AiSequence void copy (const AiSequence& sequence); @@ -105,6 +102,9 @@ namespace MWMechanics /// Removes all combat packages until first non-combat or stack empty. void stopCombat(); + /// Removes all combat packages with the given targets + void stopCombat(const std::vector& targets); + /// Has a package been completed during the last update? bool isPackageDone() const; diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 2594adcb34..0b4a1411eb 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -34,8 +34,8 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) namespace MWMechanics { - AiTravel::AiTravel(float x, float y, float z, AiTravel*) - : mX(x), mY(y), mZ(z), mHidden(false) + AiTravel::AiTravel(float x, float y, float z, bool repeat, AiTravel*) + : TypedAiPackage(repeat), mX(x), mY(y), mZ(z), mHidden(false) { } @@ -44,13 +44,13 @@ namespace MWMechanics { } - AiTravel::AiTravel(float x, float y, float z) - : AiTravel(x, y, z, this) + AiTravel::AiTravel(float x, float y, float z, bool repeat) + : AiTravel(x, y, z, repeat, this) { } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) - : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false) + : TypedAiPackage(travel->mRepeat), mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(false) { // Hidden ESM::AiSequence::AiTravel package should be converted into MWMechanics::AiInternalTravel type assert(!travel->mHidden); @@ -125,6 +125,7 @@ namespace MWMechanics travel->mData.mY = mY; travel->mData.mZ = mZ; travel->mHidden = mHidden; + travel->mRepeat = getRepeat(); ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index ee4ff9ad4d..303df7b105 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -19,11 +19,11 @@ namespace MWMechanics class AiTravel : public TypedAiPackage { public: - AiTravel(float x, float y, float z, AiTravel* derived); + AiTravel(float x, float y, float z, bool repeat, AiTravel* derived); AiTravel(float x, float y, float z, AiInternalTravel* derived); - AiTravel(float x, float y, float z); + AiTravel(float x, float y, float z, bool repeat); explicit AiTravel(const ESM::AiSequence::AiTravel* travel); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 9c84555404..664ae105f3 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include "../mwbase/world.hpp" @@ -105,7 +105,7 @@ namespace MWMechanics } AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): - TypedAiPackage(makeDefaultOptions().withRepeat(repeat)), + TypedAiPackage(repeat), mDistance(std::max(0, distance)), mDuration(std::max(0, duration)), mRemainingDuration(duration), mTimeOfDay(timeOfDay), @@ -337,7 +337,8 @@ namespace MWMechanics if (!isWaterCreature && !isFlyingCreature) { // findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance - if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, mInitialActorPosition, wanderDistance, navigatorFlags)) + if (const auto destination = DetourNavigator::findRandomPointAroundCircle(*navigator, halfExtents, + mInitialActorPosition, wanderDistance, navigatorFlags)) mDestination = *destination; else mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index ed26e2b9a4..0cbe5c284a 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -383,7 +383,7 @@ void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, Ju bool CharacterController::onOpen() { - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + if (mPtr.getType() == ESM::Container::sRecordId) { if (!mAnimation->hasAnimation("containeropen")) return true; @@ -404,7 +404,7 @@ bool CharacterController::onOpen() void CharacterController::onClose() { - if (mPtr.getTypeName() == typeid(ESM::Container).name()) + if (mPtr.getType() == ESM::Container::sRecordId) { if (!mAnimation->hasAnimation("containerclose")) return; @@ -591,7 +591,7 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. std::string anim = mCurrentMovement; mAdjustMovementAnimSpeed = true; - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() + if (mPtr.getClass().getType() == ESM::Creature::sRecordId && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) { CharacterState walkState = runStateToWalkState(mMovementState); @@ -1432,7 +1432,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); - isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); + isWeapon = (weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId); if (isWeapon) { weapSpeed = weapon->get()->mBase->mData.mSpeed; @@ -1467,7 +1467,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) mAttackStrength = 0; // Randomize attacks for non-bipedal creatures with Weapon flag - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() && + if (mPtr.getClass().getType() == ESM::Creature::sRecordId && !mPtr.getClass().isBipedal(mPtr) && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) { @@ -1590,9 +1590,9 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if(!target.isEmpty()) { - if(item.getTypeName() == typeid(ESM::Lockpick).name()) + if(item.getType() == ESM::Lockpick::sRecordId) Security(mPtr).pickLock(target, item, resultMessage, resultSound); - else if(item.getTypeName() == typeid(ESM::Probe).name()) + else if(item.getType() == ESM::Probe::sRecordId) Security(mPtr).probeTrap(target, item, resultMessage, resultSound); } mAnimation->play(mCurrentWeapon, priorityWeapon, @@ -1867,7 +1867,7 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() + if(torch != inv.end() && torch->getType() == ESM::Light::sRecordId && updateCarriedLeftVisible(mWeaponType)) { if (mAnimation->isPlaying("shield")) @@ -2044,7 +2044,7 @@ void CharacterController::update(float duration) mIsMovingBackward = vec.y() < 0; float maxDelta = osg::PI * duration * (2.5f - cosDelta); - delta = osg::clampBetween(delta, -maxDelta, maxDelta); + delta = std::clamp(delta, -maxDelta, maxDelta); stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } @@ -2061,7 +2061,7 @@ void CharacterController::update(float duration) vec.x() *= speed; vec.y() *= speed; - if(mHitState != CharState_None && mJumpState == JumpState_None) + if(mHitState != CharState_None && mHitState != CharState_Block && mJumpState == JumpState_None) vec = osg::Vec3f(); CharacterState movestate = CharState_None; @@ -2286,7 +2286,7 @@ void CharacterController::update(float duration) float swimmingPitch = mAnimation->getBodyPitchRadians(); float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0]; float maxSwimPitchDelta = 3.0f * duration; - swimmingPitch += osg::clampBetween(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); + swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } else @@ -2522,7 +2522,7 @@ void CharacterController::unpersistAnimationState() { float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); + float time = std::clamp(anim.mTime, start, stop); complete = (time - start) / (stop - start); } @@ -2746,7 +2746,7 @@ void CharacterController::setVisibility(float visibility) float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); if (chameleon) { - alpha *= std::min(0.75f, std::max(0.25f, (100.f - chameleon)/100.f)); + alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f); } visibility = std::min(visibility, alpha); @@ -2965,8 +2965,8 @@ void CharacterController::updateHeadTracking(float duration) const double xLimit = osg::DegreesToRadians(40.0); const double zLimit = osg::DegreesToRadians(30.0); double zLimitOffset = mAnimation->getUpperBodyYawRadians(); - xAngleRadians = osg::clampBetween(xAngleRadians, -xLimit, xLimit); - zAngleRadians = osg::clampBetween(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); + xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit); + zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset); float factor = duration*5; factor = std::min(factor, 1.f); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 9e5774ea9e..2962a25ede 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -47,7 +47,10 @@ namespace MWMechanics { MWMechanics::CastSpell cast(attacker, victim, fromProjectile); cast.mHitPosition = hitPosition; - cast.cast(object, false); + cast.cast(object, 0, false); + // Apply magic effects directly instead of waiting a frame to allow soul trap to work on one-hit kills + if(!victim.isEmpty() && victim.getClass().isActor()) + MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(victim); return true; } } @@ -71,7 +74,7 @@ namespace MWMechanics MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) return false; if (!blocker.getRefData().getBaseNode()) @@ -110,10 +113,9 @@ namespace MWMechanics + 0.1f * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); attackerTerm *= attackerStats.getFatigueTerm(); - int x = int(blockerTerm - attackerTerm); - int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); - int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); - x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + const int iBlockMaxChance = gmst.find("iBlockMaxChance")->mValue.getInteger(); + const int iBlockMinChance = gmst.find("iBlockMinChance")->mValue.getInteger(); + int x = std::clamp(blockerTerm - attackerTerm, iBlockMinChance, iBlockMaxChance); if (Misc::Rng::roll0to99() < x) { diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 8ca6e8b766..d832c47c87 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -7,6 +7,7 @@ #include #include +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" @@ -22,7 +23,7 @@ namespace MWMechanics mTalkedTo (false), mAlarmed (false), mAttacked (false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), - mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), + mFallHeight(0), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), mDeathAnimation(-1), mTimeOfDeath(), mSideMovementAngle(0), mLevel (0) { for (int i=0; i<4; ++i) @@ -146,7 +147,7 @@ namespace MWMechanics mAttributes[index] = value; if (index == ESM::Attribute::Intelligence) - mRecalcMagicka = true; + recalculateMagicka(); else if (index == ESM::Attribute::Strength || index == ESM::Attribute::Willpower || index == ESM::Attribute::Agility || @@ -208,11 +209,10 @@ namespace MWMechanics void CreatureStats::modifyMagicEffects(const MagicEffects &effects) { - if (effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() - != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier()) - mRecalcMagicka = true; - + bool recalc = effects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier() != mMagicEffects.get(ESM::MagicEffect::FortifyMaximumMagicka).getModifier(); mMagicEffects.setModifiers(effects); + if(recalc) + recalculateMagicka(); } void CreatureStats::setAiSetting (AiSetting index, Stat value) @@ -400,19 +400,26 @@ namespace MWMechanics return height; } - bool CreatureStats::needToRecalcDynamicStats() + void CreatureStats::recalculateMagicka() { - if (mRecalcMagicka) - { - mRecalcMagicka = false; - return true; - } - return false; - } + auto world = MWBase::Environment::get().getWorld(); + float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); - void CreatureStats::setNeedRecalcDynamicStats(bool val) - { - mRecalcMagicka = val; + float base = 1.f; + const auto& player = world->getPlayerPtr(); + if (this == &player.getClass().getCreatureStats(player)) + base = world->getStore().get().find("fPCbaseMagickaMult")->mValue.getFloat(); + else + base = world->getStore().get().find("fNPCbaseMagickaMult")->mValue.getFloat(); + + double magickaFactor = base + mMagicEffects.get(EffectKey(ESM::MagicEffect::FortifyMaximumMagicka)).getMagnitude() * 0.1; + + DynamicStat magicka = getMagicka(); + float diff = (static_cast(magickaFactor*intelligence)) - magicka.getBase(); + float currentToBaseRatio = magicka.getBase() > 0 ? magicka.getCurrent() / magicka.getBase() : 0; + magicka.setModified(magicka.getModified() + diff, 0); + magicka.setCurrent(magicka.getBase() * currentToBaseRatio, false, true); + setMagicka(magicka); } void CreatureStats::setKnockedDown(bool value) @@ -532,7 +539,7 @@ namespace MWMechanics state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; - state.mRecalcDynamicStats = mRecalcMagicka; + state.mRecalcDynamicStats = false; state.mDrawState = mDrawState; state.mLevel = mLevel; state.mActorId = mActorId; @@ -586,7 +593,6 @@ namespace MWMechanics mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; mLastHitAttemptObject = state.mLastHitAttemptObject; - mRecalcMagicka = state.mRecalcDynamicStats; mDrawState = DrawState_(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; @@ -627,6 +633,8 @@ namespace MWMechanics if (state.mHasAiSettings) for (int i=0; i<4; ++i) mAiSettings[i].readState(state.mAiSettings[i]); + if(state.mRecalcDynamicStats) + recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index d234d14486..e5c4f6fa41 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -65,8 +65,6 @@ namespace MWMechanics std::string mLastHitObject; // The last object to hit this actor std::string mLastHitAttemptObject; // The last object to attempt to hit this actor - bool mRecalcMagicka; - // For merchants: the last time items were restocked and gold pool refilled. MWWorld::TimeStamp mLastRestock; @@ -103,8 +101,7 @@ namespace MWMechanics DrawState_ getDrawState() const; void setDrawState(DrawState_ state); - bool needToRecalcDynamicStats(); - void setNeedRecalcDynamicStats(bool val); + void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); diff --git a/apps/openmw/mwmechanics/difficultyscaling.cpp b/apps/openmw/mwmechanics/difficultyscaling.cpp index 2376989745..e973e0ed52 100644 --- a/apps/openmw/mwmechanics/difficultyscaling.cpp +++ b/apps/openmw/mwmechanics/difficultyscaling.cpp @@ -13,9 +13,7 @@ float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr const MWWorld::Ptr& player = MWMechanics::getPlayer(); // [-500, 500] - int difficultySetting = Settings::Manager::getInt("difficulty", "Game"); - difficultySetting = std::min(difficultySetting, 500); - difficultySetting = std::max(difficultySetting, -500); + const int difficultySetting = std::clamp(Settings::Manager::getInt("difficulty", "Game"), -500, 500); static const float fDifficultyMult = MWBase::Environment::get().getWorld()->getStore().get().find("fDifficultyMult")->mValue.getFloat(); diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 1717ba06fe..a1870cbdb0 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -22,6 +22,7 @@ namespace MWMechanics Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) + , mObjectType(0) , mWeaponType(-1) {} @@ -29,11 +30,11 @@ namespace MWMechanics { mOldItemPtr=oldItem; mWeaponType = -1; - mObjectType.clear(); + mObjectType = 0; if(!itemEmpty()) { - mObjectType = mOldItemPtr.getTypeName(); - if (mObjectType == typeid(ESM::Weapon).name()) + mObjectType = mOldItemPtr.getType(); + if (mObjectType == ESM::Weapon::sRecordId) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } @@ -115,7 +116,7 @@ namespace MWMechanics const bool powerfulSoul = getGemCharge() >= \ MWBase::Environment::get().getWorld()->getStore().get().find ("iSoulAmountForConstantEffect")->mValue.getInteger(); - if ((mObjectType == typeid(ESM::Armor).name()) || (mObjectType == typeid(ESM::Clothing).name())) + if ((mObjectType == ESM::Armor::sRecordId) || (mObjectType == ESM::Clothing::sRecordId)) { // Armor or Clothing switch(mCastStyle) { @@ -150,7 +151,7 @@ namespace MWMechanics return; } } - else if(mObjectType == typeid(ESM::Book).name()) + else if(mObjectType == ESM::Book::sRecordId) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; @@ -355,10 +356,10 @@ namespace MWMechanics ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { - static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game"))); + static const float multiplier = std::clamp(Settings::Manager::getFloat("projectiles enchant multiplier", "Game"), 0.f, 1.f); MWWorld::Ptr player = getPlayer(); - int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); - count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints))); + count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); + count = std::clamp(getGemCharge() * multiplier / enchantPoints, 1, count); } } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 33a2820938..256c5dad48 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -23,7 +23,7 @@ namespace MWMechanics ESM::EffectList mEffectList; std::string mNewItemName; - std::string mObjectType; + unsigned int mObjectType; int mWeaponType; const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const; diff --git a/apps/openmw/mwmechanics/levelledlist.hpp b/apps/openmw/mwmechanics/levelledlist.hpp index c8368101a7..25064123ce 100644 --- a/apps/openmw/mwmechanics/levelledlist.hpp +++ b/apps/openmw/mwmechanics/levelledlist.hpp @@ -66,14 +66,14 @@ namespace MWMechanics // Is this another levelled item or a real item? MWWorld::ManualRef ref (MWBase::Environment::get().getWorld()->getStore(), item, 1); - if (ref.getPtr().getTypeName() != typeid(ESM::ItemLevList).name() - && ref.getPtr().getTypeName() != typeid(ESM::CreatureLevList).name()) + if (ref.getPtr().getType() != ESM::ItemLevList::sRecordId + && ref.getPtr().getType() != ESM::CreatureLevList::sRecordId) { return item; } else { - if (ref.getPtr().getTypeName() == typeid(ESM::ItemLevList).name()) + if (ref.getPtr().getType() == ESM::ItemLevList::sRecordId) return getLevelledItem(ref.getPtr().get()->mBase, false, seed); else return getLevelledItem(ref.getPtr().get()->mBase, true, seed); diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index e75361628b..7a1acb1003 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -1,10 +1,20 @@ #include "magiceffects.hpp" +#include #include #include #include +namespace +{ + // Round value to prevent precision issues + void truncate(float& value) + { + value = static_cast(value * 1024.f) / 1024.f; + } +} + namespace MWMechanics { EffectKey::EffectKey() : mId (0), mArg (-1) {} @@ -74,6 +84,7 @@ namespace MWMechanics { mModifier += param.mModifier; mBase += param.mBase; + truncate(mModifier); return *this; } @@ -81,6 +92,7 @@ namespace MWMechanics { mModifier -= param.mModifier; mBase -= param.mBase; + truncate(mModifier); return *this; } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 5e18e737df..e450e76483 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -77,7 +77,7 @@ namespace MWMechanics MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats (ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats (ptr); - npcStats.setNeedRecalcDynamicStats(true); + npcStats.recalculateMagicka(); const ESM::NPC *player = ptr.get()->mBase; @@ -222,7 +222,6 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); - mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { @@ -290,13 +289,8 @@ namespace MWMechanics MWWorld::Ptr ptr = getPlayer(); MWBase::WindowManager *winMgr = MWBase::Environment::get().getWindowManager(); - // Update the equipped weapon icon MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end()) - winMgr->unsetSelectedWeapon(); - else - winMgr->setSelectedWeapon(*weapon); // Update the selected spell icon MWWorld::ContainerStoreIterator enchantItem = inv.getSelectedEnchantItem(); @@ -311,6 +305,12 @@ namespace MWMechanics winMgr->unsetSelectedSpell(); } + // Update the equipped weapon icon + if (weapon == inv.end()) + winMgr->unsetSelectedWeapon(); + else + winMgr->setSelectedWeapon(*weapon); + if (mUpdatePlayer) { mUpdatePlayer = false; @@ -546,16 +546,16 @@ namespace MWMechanics x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - if(clamp) - return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return int(x); + if (clamp) + return std::clamp(x, 0, 100);//, normally clamped to [0..100] when used + return static_cast(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) { // Make sure zero base price items/services can't be bought/sold for 1 gold // and return the intended base price for creature merchants - if (basePrice == 0 || ptr.getTypeName() == typeid(ESM::Creature).name()) + if (basePrice == 0 || ptr.getType() == ESM::Creature::sRecordId) return basePrice; const MWMechanics::NpcStats &sellerStats = ptr.getClass().getNpcStats(ptr); @@ -650,9 +650,9 @@ namespace MWMechanics int flee = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting(MWMechanics::CreatureStats::AI_Fight).getBase(); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + int(std::max(iPerMinChange, s))))); + std::clamp(flee + int(std::max(iPerMinChange, s)), 0, 100)); npcStats.setAiSetting (MWMechanics::CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + int(std::min(-iPerMinChange, -s))))); + std::clamp(fight + int(std::min(-iPerMinChange, -s)), 0, 100)); } float c = -std::abs(floor(r * fPerDieRollMult)); @@ -690,10 +690,10 @@ namespace MWMechanics float s = c * fPerDieRollMult * fPerTempMult; int flee = npcStats.getAiSetting (CreatureStats::AI_Flee).getBase(); int fight = npcStats.getAiSetting (CreatureStats::AI_Fight).getBase(); - npcStats.setAiSetting (CreatureStats::AI_Flee, - std::max(0, std::min(100, flee + std::min(-int(iPerMinChange), int(-s))))); - npcStats.setAiSetting (CreatureStats::AI_Fight, - std::max(0, std::min(100, fight + std::max(int(iPerMinChange), int(s))))); + npcStats.setAiSetting(CreatureStats::AI_Flee, + std::clamp(flee + std::min(-int(iPerMinChange), int(-s)), 0, 100)); + npcStats.setAiSetting(CreatureStats::AI_Fight, + std::clamp(fight + std::max(int(iPerMinChange), int(s)), 0, 100)); } x = floor(-c * fPerDieRollMult); @@ -718,7 +718,7 @@ namespace MWMechanics if (currentDisposition + tempChange < 0) { cappedDispositionChange = -currentDisposition; - tempChange = 0; + tempChange = cappedDispositionChange; } permChange = floor(cappedDispositionChange / fPerTempMult); @@ -1608,6 +1608,11 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(ptr, "attack"); } + void MechanicsManager::stopCombat(const MWWorld::Ptr& actor) + { + mActors.stopCombat(actor); + } + void MechanicsManager::getObjectsInRange(const osg::Vec3f &position, float radius, std::vector &objects) { mActors.getObjectsInRange(position, radius, objects); @@ -1761,17 +1766,6 @@ namespace MWMechanics MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - if (actor == player->getPlayer()) - { - if (werewolf) - { - player->saveStats(); - player->setWerewolfStats(); - } - else - player->restoreStats(); - } - // Werewolfs can not cast spells, so we need to unset the prepared spell if there is one. if (npcStats.getDrawState() == MWMechanics::DrawState_Spell) npcStats.setDrawState(MWMechanics::DrawState_Nothing); @@ -1798,13 +1792,23 @@ namespace MWMechanics // Update the GUI only when called on the player MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + // Transforming removes all temporary effects + actor.getClass().getCreatureStats(actor).getActiveSpells().purge([] (const auto& params) + { + return params.getType() == ESM::ActiveSpells::Type_Consumable || params.getType() == ESM::ActiveSpells::Type_Temporary; + }, actor); + mActors.updateActor(actor, 0.f); + if (werewolf) { + player->saveStats(); + player->setWerewolfStats(); windowManager->forceHide(MWGui::GW_Inventory); windowManager->forceHide(MWGui::GW_Magic); } else { + player->restoreStats(); windowManager->unsetForceHide(MWGui::GW_Inventory); windowManager->unsetForceHide(MWGui::GW_Magic); } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 06da2fde51..0f4c2e606a 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -102,6 +102,8 @@ namespace MWMechanics /// Makes \a ptr fight \a target. Also shouts a combat taunt. void startCombat (const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; + void stopCombat(const MWWorld::Ptr& ptr) override; + /** * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. diff --git a/apps/openmw/mwmechanics/npcstats.cpp b/apps/openmw/mwmechanics/npcstats.cpp index 5d19368bf6..1d1dfacce8 100644 --- a/apps/openmw/mwmechanics/npcstats.cpp +++ b/apps/openmw/mwmechanics/npcstats.cpp @@ -371,7 +371,7 @@ int MWMechanics::NpcStats::getReputation() const void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine - mReputation = std::min(255, std::max(0, reputation)); + mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index eea0655dd8..9125eb527b 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -84,7 +84,7 @@ void Objects::update(float duration, bool paused) for(auto& object : mObjects) { - if (object.first.getTypeName() != typeid(ESM::Container).name()) + if (object.first.getType() != ESM::Container::sRecordId) continue; if (object.second->isAnimPlaying("containeropen")) diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 4b06993a49..e727d8585d 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -3,8 +3,7 @@ #include #include -#include -#include +#include #include #include @@ -114,7 +113,7 @@ namespace bool operator()(const osg::Vec3f& start, const osg::Vec3f& end) const { - const auto position = mNavigator->raycast(mHalfExtents, start, end, mFlags); + const auto position = DetourNavigator::raycast(*mNavigator, mHalfExtents, start, end, mFlags); return position.has_value() && std::abs((position.value() - start).length2() - (end - start).length2()) <= 1; } }; @@ -422,8 +421,8 @@ namespace MWMechanics const auto world = MWBase::Environment::get().getWorld(); const auto stepSize = getPathStepSize(actor); const auto navigator = world->getNavigator(); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts, - endTolerance, out); + const auto status = DetourNavigator::findPath(*navigator, halfExtents, stepSize, + startPoint, endPoint, flags, areaCosts, endTolerance, out); if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) return DetourNavigator::Status::Success; @@ -455,8 +454,8 @@ namespace MWMechanics std::deque prePath; auto prePathInserter = std::back_inserter(prePath); const float endTolerance = 0; - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, mPath.front(), flags, areaCosts, - endTolerance, prePathInserter); + const auto status = DetourNavigator::findPath(*navigator, halfExtents, stepSize, + startPoint, mPath.front(), flags, areaCosts, endTolerance, prePathInserter); if (status == DetourNavigator::Status::NavMeshNotFound) return; diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp deleted file mode 100644 index 82531132ca..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "spellabsorption.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "creaturestats.hpp" -#include "spellutil.hpp" - -namespace MWMechanics -{ - float getProbability(const MWMechanics::ActiveSpells& activeSpells) - { - float probability = 0.f; - for(const auto& params : activeSpells) - { - for(const auto& effect : params.getEffects()) - { - if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) - { - if(probability == 0.f) - probability = effect.mMagnitude / 100; - else - { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - probability; - failProbability *= 1.f - effect.mMagnitude / 100; - probability = 1.f - failProbability; - } - } - } - } - return static_cast(probability * 100); - } - - bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) - { - if (spellId.empty() || target.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - CreatureStats& stats = target.getClass().getCreatureStats(target); - if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) - return false; - - int chance = getProbability(stats.getActiveSpells()); - if (Misc::Rng::roll0to99() >= chance) - return false; - - const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); - const ESM::Spell* spell = esmStore.get().search(spellId); - int spellCost = 0; - if (spell) - { - spellCost = MWMechanics::calcSpellCost(*spell); - } - else - { - const ESM::Enchantment* enchantment = esmStore.get().search(spellId); - if (enchantment) - spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); - } - - // Magicka is increased by the cost of the spell - DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spellCost); - stats.setMagicka(magicka); - return true; - } - -} diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp deleted file mode 100644 index 0fe501df91..0000000000 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef MWMECHANICS_SPELLABSORPTION_H -#define MWMECHANICS_SPELLABSORPTION_H - -#include - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - // Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. - bool absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); -} - -#endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 53fbe69ab3..2edc437752 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,38 +22,11 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "spellabsorption.hpp" #include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" #include "weapontype.hpp" -namespace -{ - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } -} - namespace MWMechanics { CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell) @@ -82,7 +55,7 @@ namespace MWMechanics } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, - const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) + const ESM::EffectList &effects, ESM::RangeType range, bool exploded) { const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) @@ -123,7 +96,6 @@ namespace MWMechanics } } - ESM::EffectList reflectedEffects; ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); @@ -136,9 +108,6 @@ namespace MWMechanics // throughout the iteration of this spell's // effects, we display a "can't re-cast" message - // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(mId, caster, target); - int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) @@ -167,19 +136,6 @@ namespace MWMechanics && (caster.isEmpty() || !caster.getClass().isActor())) continue; - // Notify the target actor they've been hit - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); - - // Avoid proceeding further for absorbed spells. - if (absorbed) - continue; - - // Reflect harmful effects - if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) - continue; - ActiveSpells::ActiveEffect effect; effect.mEffectId = effectIt->mEffectID; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; @@ -189,13 +145,8 @@ namespace MWMechanics effect.mTimeLeft = 0.f; effect.mEffectIndex = currentEffectIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMinMagnitude = 0; - effect.mMaxMagnitude = 0; - } + if(mManualSpell) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; @@ -209,14 +160,14 @@ namespace MWMechanics // add to list of active effects, to apply in next frame params.getEffects().emplace_back(effect); - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. MWBase::Environment::get().getWindowManager()->setEnemy(target); } - if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { playEffects(target, *magicEffect); } @@ -227,9 +178,6 @@ namespace MWMechanics if (!target.isEmpty()) { - if (!reflectedEffects.mList.empty()) - inflict(caster, target, reflectedEffects, range, true, exploded); - if (!params.getEffects().empty()) { if(targetIsActor) @@ -237,6 +185,7 @@ namespace MWMechanics else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + // and we can ignore reflection since non-actors cannot reflect spells for(auto& effect : params.getEffects()) applyMagicEffect(target, caster, params, effect, 0.f); } @@ -274,7 +223,7 @@ namespace MWMechanics bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; - if (item.getTypeName() == typeid(ESM::Weapon).name()) + if (item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; @@ -333,7 +282,10 @@ namespace MWMechanics mCaster.getClass().skillUsageSucceeded (mCaster, ESM::Skill::Enchant, 3); } - inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); + if (isProjectile) + inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Self); + else + inflict(mCaster, mCaster, enchantment->mEffects, ESM::RT_Self); if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Touch); diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index a21ea33e0b..4d9cc3a7de 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -62,7 +62,7 @@ namespace MWMechanics /// @note \a target can be any type of object, not just actors. /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); + const ESM::EffectList& effects, ESM::RangeType range, bool exploded=false); }; void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index d9e72ee43b..3b352a710b 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -16,6 +16,7 @@ #include "../mwmechanics/aifollow.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwrender/animation.hpp" @@ -213,8 +214,8 @@ namespace if (!wasEquipped) return; - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) + auto type = currentItem->getType(); + if (type != ESM::Weapon::sRecordId && type != ESM::Armor::sRecordId && type != ESM::Clothing::sRecordId) return; if (actor.getClass().getCreatureStats(actor).isDead()) @@ -261,6 +262,97 @@ namespace return false; } + void absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + { + const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation && !absorbStatic->mModel.empty()) + animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); + const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; + if (spell) + { + spellCost = MWMechanics::calcSpellCost(*spell); + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + if (enchantment) + spellCost = MWMechanics::getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); + } + + // Magicka is increased by the cost of the spell + auto& stats = target.getClass().getCreatureStats(target); + auto magicka = stats.getMagicka(); + magicka.setCurrent(magicka.getCurrent() + spellCost); + stats.setMagicka(magicka); + } + + MWMechanics::MagicApplicationResult applyProtections(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, + const MWMechanics::ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, const ESM::MagicEffect* magicEffect) + { + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + // Apply reflect and spell absorption + if(target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment && spellParams.getType() != ESM::ActiveSpells::Type_Permanent) + { + bool canReflect = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && + !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) && magnitudes.get(ESM::MagicEffect::Reflect).getMagnitude() > 0.f; + bool canAbsorb = !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_SpellAbsorption) && magnitudes.get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f; + if(canReflect || canAbsorb) + { + for(const auto& activeParam : stats.getActiveSpells()) + { + for(const auto& activeEffect : activeParam.getEffects()) + { + if(!(activeEffect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + if(activeEffect.mEffectId == ESM::MagicEffect::Reflect) + { + if(canReflect && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + return MWMechanics::MagicApplicationResult::REFLECTED; + } + } + else if(activeEffect.mEffectId == ESM::MagicEffect::SpellAbsorption) + { + if(canAbsorb && Misc::Rng::roll0to99() < activeEffect.mMagnitude) + { + absorbSpell(spellParams.getId(), caster, target); + return MWMechanics::MagicApplicationResult::REMOVED; + } + } + } + } + } + } + // Notify the target actor they've been hit + bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; + if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) + target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); + // Apply resistances + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) + { + const ESM::Spell* spell = nullptr; + if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellParams.getId()); + float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); + if (magnitudeMult == 0) + { + // Fully resisted, show message + if (target == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); + else if (caster == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + return MWMechanics::MagicApplicationResult::REMOVED; + } + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } + return MWMechanics::MagicApplicationResult::APPLIED; + } + static const std::map sBoundItemsMap{ {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, @@ -279,7 +371,7 @@ namespace namespace MWMechanics { -void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage, bool& recalculateMagicka) { const auto world = MWBase::Environment::get().getWorld(); bool godmode = target == getPlayer() && world->getGodModeState(); @@ -539,7 +631,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co if (!target.isInCell() || !target.getCell()->isExterior() || godmode) break; float time = world->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float timeDiff = std::clamp(std::abs(time - 13.f), 0.f, 7.f); float damageScale = 1.f - timeDiff / 7.f; // When cloudy, the sun damage effect is halved static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); @@ -609,7 +701,7 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co fortifySkill(target, effect, effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + recalculateMagicka = true; break; case ESM::MagicEffect::AbsorbHealth: case ESM::MagicEffect::AbsorbMagicka: @@ -682,28 +774,38 @@ void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, co } } -bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) +MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) { const auto world = MWBase::Environment::get().getWorld(); bool invalid = false; bool receivedMagicDamage = false; + bool recalculateMagicka = false; if(effect.mEffectId == ESM::MagicEffect::Corprus && spellParams.shouldWorsen()) { spellParams.worsen(); for(auto& otherEffect : spellParams.getEffects()) { if(isCorprusEffect(otherEffect)) - applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, otherEffect, invalid, receivedMagicDamage, recalculateMagicka); } if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - return false; + return MagicApplicationResult::APPLIED; } else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) { if(target == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - return true; + onMagicEffectRemoved(target, spellParams, effect); + return MagicApplicationResult::REMOVED; + } + else if(effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention || effect.mEffectId == ESM::MagicEffect::DivineIntervention || effect.mEffectId == ESM::MagicEffect::Recall) + { + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + { + onMagicEffectRemoved(target, spellParams, effect); + return MagicApplicationResult::REMOVED; + } } const auto* magicEffect = world->getStore().get().find(effect.mEffectId); if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) @@ -711,10 +813,10 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) { effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } else if(!dt) - return false; + return MagicApplicationResult::APPLIED; } if(effect.mEffectId == ESM::MagicEffect::Lock) { @@ -770,28 +872,19 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac } else { - auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); - if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + auto& stats = target.getClass().getCreatureStats(target); + auto& magnitudes = stats.getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) { - const ESM::Spell* spell = nullptr; - if(spellParams.getType() == ESM::ActiveSpells::Type_Temporary) - spell = world->getStore().get().search(spellParams.getId()); - float magnitudeMult = getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); - if (magnitudeMult == 0) - { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); - return true; - } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + MagicApplicationResult result = applyProtections(target, caster, spellParams, effect, magicEffect); + if(result != MagicApplicationResult::APPLIED) + return result; } float oldMagnitude = 0.f; if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) oldMagnitude = effect.mMagnitude; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + playEffects(target, *magicEffect); float magnitude = roll(effect); //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here effect.mMagnitude = magnitude; @@ -806,15 +899,16 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac } if(effect.mMagnitude == 0) { + effect.mMagnitude = oldMagnitude; effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; effect.mTimeLeft -= dt; - return false; + return MagicApplicationResult::APPLIED; } } if(effect.mEffectId == ESM::MagicEffect::Corprus) spellParams.worsen(); else - applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage, recalculateMagicka); effect.mMagnitude = magnitude; magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); } @@ -831,7 +925,9 @@ bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, Ac effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; if (receivedMagicDamage && target == getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return false; + if(recalculateMagicka) + target.getClass().getCreatureStats(target).recalculateMagicka(); + return MagicApplicationResult::APPLIED; } void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) @@ -980,7 +1076,7 @@ void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellPara fortifySkill(target, effect, -effect.mMagnitude); break; case ESM::MagicEffect::FortifyMaximumMagicka: - target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + target.getClass().getCreatureStats(target).recalculateMagicka(); break; case ESM::MagicEffect::AbsorbAttribute: { @@ -1027,15 +1123,15 @@ void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellP { if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) return; - const auto world = MWBase::Environment::get().getWorld(); auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); - const auto* magicEffect = world->getStore().get().find(effect.mEffectId); - if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) - magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMagnitude)); + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(-effect.mMagnitude)); removeMagicEffect(target, spellParams, effect); - auto anim = world->getAnimation(target); - if(anim) - anim->removeEffect(effect.mEffectId); + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } } -} \ No newline at end of file +} diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp index a9e7b066f3..2861d0d64a 100644 --- a/apps/openmw/mwmechanics/spelleffects.hpp +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -10,8 +10,13 @@ namespace MWMechanics { + enum class MagicApplicationResult + { + APPLIED, REMOVED, REFLECTED + }; + // Applies a tick of a single effect. Returns true if the effect should be removed immediately - bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 974d297f10..acc9fc2a3f 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -96,7 +96,7 @@ namespace MWMechanics float ratePotion (const MWWorld::Ptr &item, const MWWorld::Ptr& actor) { - if (item.getTypeName() != typeid(ESM::Potion).name()) + if (item.getType() != ESM::Potion::sRecordId) return 0.f; const ESM::Potion* potion = item.get()->mBase; @@ -368,6 +368,24 @@ namespace MWMechanics return 0.f; break; + case ESM::MagicEffect::BoundShield: + if(!actor.getClass().hasInventoryStore(actor)) + return 0.f; + else if(!actor.getClass().isNpc()) + { + // If the actor is an NPC they can benefit from the armor rating, otherwise check if we've got a one-handed weapon to use with the shield + const auto& store = actor.getClass().getInventoryStore(actor); + auto oneHanded = std::find_if(store.cbegin(MWWorld::ContainerStore::Type_Weapon), store.cend(), [](const MWWorld::ConstPtr& weapon) + { + if(weapon.getClass().getItemHealth(weapon) <= 0.f) + return false; + short type = weapon.get()->mBase->mData.mType; + return !(MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded); + }); + if(oneHanded == store.cend()) + return 0.f; + } + break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: @@ -552,6 +570,13 @@ namespace MWMechanics if (!creatureStats.getSummonedCreatureMap().empty()) return 0.f; } + if(effect.mEffectID >= ESM::MagicEffect::BoundDagger && effect.mEffectID <= ESM::MagicEffect::BoundGloves) + { + // While rateSpell prevents actors from recasting the same spell, it doesn't prevent them from casting different spells with the same effect. + // Multiple instances of the same bound item don't stack so if the effect is already active, rate it as useless. + if(actor.getClass().getCreatureStats(actor).getMagicEffects().get(effect.mEffectID).getMagnitude() > 0.f) + return 0.f; + } // Underwater casting not possible if (effect.mRange == ESM::RT_Target) diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 18a1c0004e..70167abcd3 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -162,7 +162,10 @@ namespace MWMechanics float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); - return std::max(0.f, cap ? std::min(100.f, castChance) : castChance); + if (cap) + return std::clamp(castChance, 0.f, 100.f); + + return std::max(castChance, 0.f); } float getSpellSuccessChance (const std::string& spellId, const MWWorld::Ptr& actor, int* effectiveSchool, bool cap, bool checkMagicka) @@ -214,7 +217,7 @@ namespace MWMechanics case ESM::MagicEffect::Soultrap: { if (!target.getClass().isNpc() // no messagebox for NPCs - && (target.getTypeName() == typeid(ESM::Creature).name() && target.get()->mBase->mData.mSoul == 0)) + && (target.getType() == ESM::Creature::sRecordId && target.get()->mBase->mData.mSoul == 0)) { if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInvalidTarget}"); diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp index b824d7c450..750fd803d4 100644 --- a/apps/openmw/mwmechanics/trading.cpp +++ b/apps/openmw/mwmechanics/trading.cpp @@ -23,7 +23,7 @@ namespace MWMechanics } // reject if npc is a creature - if ( merchant.getTypeName() != typeid(ESM::NPC).name() ) { + if ( merchant.getType() != ESM::NPC::sRecordId ) { return false; } diff --git a/apps/openmw/mwmechanics/typedaipackage.hpp b/apps/openmw/mwmechanics/typedaipackage.hpp index d2d424326c..0ea276999f 100644 --- a/apps/openmw/mwmechanics/typedaipackage.hpp +++ b/apps/openmw/mwmechanics/typedaipackage.hpp @@ -11,6 +11,9 @@ namespace MWMechanics TypedAiPackage() : AiPackage(T::getTypeId(), T::makeDefaultOptions()) {} + TypedAiPackage(bool repeat) : + AiPackage(T::getTypeId(), T::makeDefaultOptions().withRepeat(repeat)) {} + TypedAiPackage(const Options& options) : AiPackage(T::getTypeId(), options) {} diff --git a/apps/openmw/mwmechanics/weaponpriority.cpp b/apps/openmw/mwmechanics/weaponpriority.cpp index 8480dc208e..1a17cc87e6 100644 --- a/apps/openmw/mwmechanics/weaponpriority.cpp +++ b/apps/openmw/mwmechanics/weaponpriority.cpp @@ -20,7 +20,7 @@ namespace MWMechanics float rateWeapon (const MWWorld::Ptr &item, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, int type, float arrowRating, float boltRating) { - if (enemy.isEmpty() || item.getTypeName() != typeid(ESM::Weapon).name()) + if (enemy.isEmpty() || item.getType() != ESM::Weapon::sRecordId) return 0.f; if (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) == 0) @@ -128,8 +128,7 @@ namespace MWMechanics } // Take hit chance in account, but do not allow rating become negative. - float chance = getHitChance(actor, enemy, value) / 100.f; - rating *= std::min(1.f, std::max(0.01f, chance)); + rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; diff --git a/apps/openmw/mwmechanics/weapontype.cpp b/apps/openmw/mwmechanics/weapontype.cpp index feecd468ad..30f554f80b 100644 --- a/apps/openmw/mwmechanics/weapontype.cpp +++ b/apps/openmw/mwmechanics/weapontype.cpp @@ -21,13 +21,13 @@ namespace MWMechanics *weaptype = ESM::Weapon::HandToHand; else { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); *weaptype = ref->mBase->mData.mType; } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) *weaptype = ESM::Weapon::PickProbe; } diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index e140141e32..16501c432d 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -12,6 +12,7 @@ #include "collisiontype.hpp" #include "mtphysics.hpp" +#include "trace.h" #include @@ -21,8 +22,8 @@ namespace MWPhysics Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk) : mStandingOnPtr(nullptr), mCanWaterWalk(canWaterWalk), mWalkingOnWater(false) - , mMeshTranslation(shape->mCollisionBox.center), mOriginalHalfExtents(shape->mCollisionBox.extents) - , mVelocity(0,0,0), mStuckFrames(0), mLastStuckPosition{0, 0, 0} + , mMeshTranslation(shape->mCollisionBox.mCenter), mOriginalHalfExtents(shape->mCollisionBox.mExtents) + , mStuckFrames(0), mLastStuckPosition{0, 0, 0} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) , mExternalCollisionMode(true) @@ -123,7 +124,6 @@ void Actor::updatePosition() mSimulationPosition = worldPosition; mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; - mSkipCollisions = true; mSkipSimulation = true; } @@ -133,11 +133,6 @@ void Actor::setSimulationPosition(const osg::Vec3f& position) mSimulationPosition = position; } -osg::Vec3f Actor::getSimulationPosition() const -{ - return mSimulationPosition; -} - osg::Vec3f Actor::getScaledMeshTranslation() const { return mRotation * osg::componentMultiply(mMeshTranslation, mScale); @@ -167,17 +162,19 @@ bool Actor::setPosition(const osg::Vec3f& position) { std::scoped_lock lock(mPositionMutex); applyOffsetChange(); - bool hasChanged = mPosition != position || mWorldPositionChanged; - mPreviousPosition = mPosition; - mPosition = position; + bool hasChanged = (mPosition != position && !mSkipSimulation) || mWorldPositionChanged; + if (!mSkipSimulation) + { + mPreviousPosition = mPosition; + mPosition = position; + } return hasChanged; } -void Actor::adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions) +void Actor::adjustPosition(const osg::Vec3f& offset) { std::scoped_lock lock(mPositionMutex); mPositionOffset += offset; - mSkipCollisions = mSkipCollisions || ignoreCollisions; } void Actor::applyOffsetChange() @@ -191,16 +188,6 @@ void Actor::applyOffsetChange() mWorldPositionChanged = true; } -osg::Vec3f Actor::getPosition() const -{ - return mPosition; -} - -osg::Vec3f Actor::getPreviousPosition() const -{ - return mPreviousPosition; -} - void Actor::setRotation(osg::Quat quat) { std::scoped_lock lock(mPositionMutex); @@ -288,19 +275,15 @@ void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) mStandingOnPtr = ptr; } -bool Actor::skipCollisions() -{ - return std::exchange(mSkipCollisions, false); -} - -void Actor::setVelocity(osg::Vec3f velocity) -{ - mVelocity = velocity; -} - -osg::Vec3f Actor::velocity() +bool Actor::canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const { - return std::exchange(mVelocity, osg::Vec3f()); + const float halfZ = getHalfExtents().z(); + const osg::Vec3f actorPosition = getPosition(); + const osg::Vec3f startingPosition(actorPosition.x(), actorPosition.y(), actorPosition.z() + halfZ); + const osg::Vec3f destinationPosition(actorPosition.x(), actorPosition.y(), waterlevel + halfZ); + MWPhysics::ActorTracer tracer; + tracer.doTrace(getCollisionObject(), startingPosition, destinationPosition, world); + return (tracer.mFraction >= 1.0f); } } diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index 0846401c1d..01d8037f6b 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -12,6 +12,7 @@ class btCollisionShape; class btCollisionObject; +class btCollisionWorld; class btConvexShape; namespace Resource @@ -59,7 +60,6 @@ namespace MWPhysics * to account for e.g. scripted movements */ void setSimulationPosition(const osg::Vec3f& position); - osg::Vec3f getSimulationPosition() const; void updateCollisionObjectPosition(); @@ -89,15 +89,11 @@ namespace MWPhysics void updatePosition(); // register a position offset that will be applied during simulation. - void adjustPosition(const osg::Vec3f& offset, bool ignoreCollisions); + void adjustPosition(const osg::Vec3f& offset); // apply position offset. Can't be called during simulation void applyOffsetChange(); - osg::Vec3f getPosition() const; - - osg::Vec3f getPreviousPosition() const; - /** * Returns the half extents of the collision body (scaled according to rendering scale) * @note The reason we need this extra method is because of an inconsistency in MW - NPC race scales aren't applied to the collision shape, @@ -160,10 +156,7 @@ namespace MWPhysics mLastStuckPosition = position; } - bool skipCollisions(); - - void setVelocity(osg::Vec3f velocity); - osg::Vec3f velocity(); + bool canMoveToWaterSurface(float waterlevel, const btCollisionWorld* world) const; private: MWWorld::Ptr mStandingOnPtr; @@ -190,13 +183,8 @@ namespace MWPhysics osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mSimulationPosition; - osg::Vec3f mPosition; - osg::Vec3f mPreviousPosition; osg::Vec3f mPositionOffset; - osg::Vec3f mVelocity; bool mWorldPositionChanged; - bool mSkipCollisions; bool mSkipSimulation; mutable std::mutex mPositionMutex; diff --git a/apps/openmw/mwphysics/constants.hpp b/apps/openmw/mwphysics/constants.hpp index eaeb308d58..c275b63c7b 100644 --- a/apps/openmw/mwphysics/constants.hpp +++ b/apps/openmw/mwphysics/constants.hpp @@ -3,7 +3,6 @@ namespace MWPhysics { - static constexpr float sStepSizeUp = 34.0f; static constexpr float sStepSizeDown = 62.0f; static constexpr float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes @@ -12,7 +11,6 @@ namespace MWPhysics static constexpr bool sDoExtraStairHacks = true; static constexpr float sGroundOffset = 1.0f; - static constexpr float sMaxSlope = 49.0f; // Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared. static constexpr int sMaxIterations = 8; diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index fc8725f5f4..a01ab96301 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -15,9 +15,9 @@ namespace MWPhysics const btVector3& position, const btScalar radius) { const btVector3 nearest( - std::max(aabbMin.x(), std::min(aabbMax.x(), position.x())), - std::max(aabbMin.y(), std::min(aabbMax.y(), position.y())), - std::max(aabbMin.z(), std::min(aabbMax.z(), position.z())) + std::clamp(position.x(), aabbMin.x(), aabbMax.x()), + std::clamp(position.y(), aabbMin.y(), aabbMax.y()), + std::clamp(position.z(), aabbMin.z(), aabbMax.z()) ); return nearest.distance(position) < radius; } diff --git a/apps/openmw/mwphysics/heightfield.cpp b/apps/openmw/mwphysics/heightfield.cpp index e210bc3903..d363ddef11 100644 --- a/apps/openmw/mwphysics/heightfield.cpp +++ b/apps/openmw/mwphysics/heightfield.cpp @@ -1,6 +1,8 @@ #include "heightfield.hpp" #include "mtphysics.hpp" +#include + #include #include @@ -19,17 +21,17 @@ namespace { template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { return {}; } template - auto makeHeights(const T* heights, float sqrtVerts) + auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { - return std::vector(heights, heights + static_cast(sqrtVerts * sqrtVerts)); + return std::vector(heights, heights + static_cast(verts * verts)); } template @@ -50,16 +52,17 @@ namespace namespace MWPhysics { - HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) + HeightField::HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler) : mHoldObject(holdObject) #if BT_BULLET_VERSION < 310 - , mHeights(makeHeights(heights, sqrtVerts)) + , mHeights(makeHeights(heights, verts)) #endif , mTaskScheduler(scheduler) { #if BT_BULLET_VERSION < 310 mShape = std::make_unique( - sqrtVerts, sqrtVerts, + verts, verts, getHeights(heights, mHeights), 1, minH, maxH, 2, @@ -67,10 +70,12 @@ namespace MWPhysics ); #else mShape = std::make_unique( - sqrtVerts, sqrtVerts, heights, minH, maxH, 2, false); + verts, verts, heights, minH, maxH, 2, false); #endif mShape->setUseDiamondSubdivision(true); - mShape->setLocalScaling(btVector3(triSize, triSize, 1)); + + const float scaling = static_cast(size) / static_cast(verts - 1); + mShape->setLocalScaling(btVector3(scaling, scaling, 1)); #if BT_BULLET_VERSION >= 289 // Accelerates some collision tests. @@ -81,10 +86,8 @@ namespace MWPhysics mShape->buildAccelerator(); #endif - btTransform transform(btQuaternion::getIdentity(), - btVector3((x+0.5f) * triSize * (sqrtVerts-1), - (y+0.5f) * triSize * (sqrtVerts-1), - (maxH+minH)*0.5f)); + const btTransform transform(btQuaternion::getIdentity(), + BulletHelpers::getHeightfieldShift(x, y, size, minH, maxH)); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionShape(mShape.get()); diff --git a/apps/openmw/mwphysics/heightfield.hpp b/apps/openmw/mwphysics/heightfield.hpp index 93b2733f31..c320225258 100644 --- a/apps/openmw/mwphysics/heightfield.hpp +++ b/apps/openmw/mwphysics/heightfield.hpp @@ -23,7 +23,8 @@ namespace MWPhysics class HeightField { public: - HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); + HeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, + const osg::Object* holdObject, PhysicsTaskScheduler* scheduler); ~HeightField(); btCollisionObject* getCollisionObject(); diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index df9239dd83..fc2ea57b40 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,8 @@ #include "constants.hpp" #include "contacttestwrapper.h" #include "physicssystem.hpp" +#include "projectile.hpp" +#include "projectileconvexcallback.hpp" #include "stepper.hpp" #include "trace.h" @@ -116,7 +119,7 @@ namespace MWPhysics } void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, - WorldFrameData& worldData) + const WorldFrameData& worldData) { // Reset per-frame data actor.mWalkingOnWater = false; @@ -203,7 +206,7 @@ namespace MWPhysics if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -230,7 +233,7 @@ namespace MWPhysics float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; - if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) + if (hitHeight < Constants::sStepSizeUp && !isActor(tracer.mHitObject)) { // Try to step up onto it. // NOTE: this modifies newPosition and velocity on its own if successful @@ -338,7 +341,7 @@ namespace MWPhysics osg::Vec3f from = newPosition; auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); - tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); if(tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) @@ -398,6 +401,29 @@ namespace MWPhysics actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate } + void MovementSolver::move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld) + { + btVector3 btFrom = Misc::Convert::toBullet(projectile.mPosition); + btVector3 btTo = Misc::Convert::toBullet(projectile.mPosition + projectile.mMovement * time); + + if (btFrom == btTo) + return; + + ProjectileConvexCallback resultCallback(projectile.mCaster, projectile.mCollisionObject, btFrom, btTo, projectile.mProjectile); + resultCallback.m_collisionFilterMask = 0xff; + resultCallback.m_collisionFilterGroup = CollisionType_Projectile; + + const btQuaternion btrot = btQuaternion::getIdentity(); + btTransform from_ (btrot, btFrom); + btTransform to_ (btrot, btTo); + + const btCollisionShape* shape = projectile.mCollisionObject->getCollisionShape(); + assert(shape->isConvex()); + collisionWorld->convexSweepTest(static_cast(shape), from_, to_, resultCallback); + + projectile.mPosition = Misc::Convert::toOsg(projectile.mProjectile->isActive() ? btTo : resultCallback.m_hitPointWorld); + } + btVector3 addMarginToDelta(btVector3 delta) { if(delta.length2() == 0.0) diff --git a/apps/openmw/mwphysics/movementsolver.hpp b/apps/openmw/mwphysics/movementsolver.hpp index 90b10c275e..1bbe76cbec 100644 --- a/apps/openmw/mwphysics/movementsolver.hpp +++ b/apps/openmw/mwphysics/movementsolver.hpp @@ -3,7 +3,8 @@ #include -#include "constants.hpp" +#include + #include "../mwworld/ptr.hpp" class btCollisionWorld; @@ -30,19 +31,21 @@ namespace MWPhysics template static bool isWalkableSlope(const Vec3 &normal) { - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); return (normal.z() > sMaxSlopeCos); } class Actor; struct ActorFrameData; + struct ProjectileFrameData; struct WorldFrameData; class MovementSolver { public: static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight); - static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData); + static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData); + static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index a0dde67c2e..2f7e3b5995 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -105,10 +105,134 @@ namespace return actorData.mPosition.z() < actorData.mSwimLevel; } - osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); - return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor); + return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); + } + + namespace Visitors + { + struct InitPosition + { + const btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + if (frameData.mWaterCollision && frameData.mPosition.z() < frameData.mWaterlevel && actor->canMoveToWaterSurface(frameData.mWaterlevel, mCollisionWorld)) + { + const auto offset = osg::Vec3f(0, 0, frameData.mWaterlevel - frameData.mPosition.z()); + MWBase::Environment::get().getWorld()->moveObjectBy(actor->getPtr(), offset); + actor->applyOffsetChange(); + frameData.mPosition = actor->getPosition(); + } + frameData.mOldHeight = frameData.mPosition.z(); + const auto rotation = actor->getPtr().getRefData().getPosition().asRotationVec3(); + frameData.mRotation = osg::Vec2f(rotation.x(), rotation.z()); + frameData.mInertia = actor->getInertialForce(); + frameData.mStuckFrames = actor->getStuckFrames(); + frameData.mLastStuckPosition = actor->getLastStuckPosition(); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } + }; + + struct PreStep + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::unstuck(sim.second, mCollisionWorld); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + } + }; + + struct UpdatePosition + { + btCollisionWorld* mCollisionWorld; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + if (actor->setPosition(frameData.mPosition)) + { + frameData.mPosition = actor->getPosition(); // account for potential position change made by script + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setPosition(frameData.mPosition); + proj->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(proj->getCollisionObject()); + } + }; + + struct Move + { + const float mPhysicsDt; + const btCollisionWorld* mCollisionWorld; + const MWPhysics::WorldFrameData& mWorldFrameData; + void operator()(MWPhysics::ActorSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld, mWorldFrameData); + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + MWPhysics::MovementSolver::move(sim.second, mPhysicsDt, mCollisionWorld); + } + }; + + struct Sync + { + const bool mAdvanceSimulation; + const float mTimeAccum; + const float mPhysicsDt; + const MWPhysics::PhysicsTaskScheduler* scheduler; + void operator()(MWPhysics::ActorSimulation& sim) const + { + auto& [actor, frameData] = sim; + auto ptr = actor->getPtr(); + + MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + const float heightDiff = frameData.mPosition.z() - frameData.mOldHeight; + const bool isStillOnGround = (mAdvanceSimulation && frameData.mWasOnGround && frameData.mIsOnGround); + + if (isStillOnGround || frameData.mFlying || isUnderWater(frameData) || frameData.mSlowFall < 1) + stats.land(ptr == MWMechanics::getPlayer() && (frameData.mFlying || isUnderWater(frameData))); + else if (heightDiff < 0) + stats.addToFallHeight(-heightDiff); + + actor->setSimulationPosition(::interpolateMovements(*actor, mTimeAccum, mPhysicsDt)); + actor->setLastStuckPosition(frameData.mLastStuckPosition); + actor->setStuckFrames(frameData.mStuckFrames); + if (mAdvanceSimulation) + { + MWWorld::Ptr standingOn; + auto* ptrHolder = static_cast(scheduler->getUserPointer(frameData.mStandingOn)); + if (ptrHolder) + standingOn = ptrHolder->getPtr(); + actor->setStandingOnPtr(standingOn); + // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change + if (actor->getOnGround() == frameData.mWasOnGround) + actor->setOnGround(frameData.mIsOnGround); + actor->setOnSlope(frameData.mIsOnSlope); + actor->setWalkingOnWater(frameData.mWalkingOnWater); + actor->setInertialForce(frameData.mInertia); + } + } + void operator()(MWPhysics::ProjectileSimulation& sim) const + { + auto& [proj, frameData] = sim; + proj->setSimulationPosition(::interpolateMovements(*proj, mTimeAccum, mPhysicsDt)); + } + }; } namespace Config @@ -143,7 +267,7 @@ namespace MWPhysics , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) - , mNewFrame(false) + , mFrameCounter(0) , mAdvanceSimulation(false) , mQuit(false) , mNextJob(0) @@ -178,13 +302,14 @@ namespace MWPhysics PhysicsTaskScheduler::~PhysicsTaskScheduler() { + waitForWorkers(); { MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); mQuit = true; mNumJobs = 0; mRemainingSteps = 0; + mHasJob.notify_all(); } - mHasJob.notify_all(); for (auto& thread : mThreads) thread.join(); } @@ -235,13 +360,14 @@ namespace MWPhysics return std::make_tuple(numSteps, actualDelta); } - void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { + waitForWorkers(); + // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); - assert(actors.size() == actorsData.size()); double timeStart = mTimer->tick(); @@ -259,19 +385,19 @@ namespace MWPhysics timeAccum -= numSteps*newDelta; // init - for (size_t i = 0; i < actors.size(); ++i) + const Visitors::InitPosition vis{mCollisionWorld}; + for (auto& sim : simulations) { - actorsData[i].updatePosition(*actors[i], mCollisionWorld); + std::visit(vis, sim); } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; - mActors = std::move(actors); - mActorsFrameData = std::move(actorsData); + mSimulations = std::move(simulations); mAdvanceSimulation = (mRemainingSteps != 0); - mNewFrame = true; - mNumJobs = mActorsFrameData.size(); + ++mFrameCounter; + mNumJobs = mSimulations.size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); @@ -298,11 +424,11 @@ namespace MWPhysics void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { + waitForWorkers(); MaybeExclusiveLock lock(mSimulationMutex, mNumThreads); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); - mActors.clear(); - mActorsFrameData.clear(); + mSimulations.clear(); for (const auto& [_, actor] : actors) { actor->updatePosition(); @@ -448,18 +574,22 @@ namespace MWPhysics } else if (const auto projectile = std::dynamic_pointer_cast(ptr)) { - projectile->commitPositionChange(); + projectile->updateCollisionObjectPosition(); mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); } } void PhysicsTaskScheduler::worker() { + std::size_t lastFrame = 0; std::shared_lock lock(mSimulationMutex); while (!mQuit) { - if (!mNewFrame) - mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; }); + if (lastFrame == mFrameCounter) + { + mHasJob.wait(lock, [&] { return mQuit || lastFrame != mFrameCounter; }); + lastFrame = mFrameCounter; + } doSimulation(); } @@ -467,47 +597,11 @@ namespace MWPhysics void PhysicsTaskScheduler::updateActorsPositions() { - for (size_t i = 0; i < mActors.size(); ++i) - { - if (mActors[i]->setPosition(mActorsFrameData[i].mPosition)) - { - MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - mActorsFrameData[i].mPosition = mActors[i]->getPosition(); // account for potential position change made by script - mActors[i]->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(mActors[i]->getCollisionObject()); - } - } - } - - void PhysicsTaskScheduler::updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const - { - auto ptr = actor.getPtr(); - - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); - - if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); - else if (heightDiff < 0) - stats.addToFallHeight(-heightDiff); - - actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt)); - actor.setLastStuckPosition(actorData.mLastStuckPosition); - actor.setStuckFrames(actorData.mStuckFrames); - if (simulationPerformed) + const Visitors::UpdatePosition vis{mCollisionWorld}; + for (auto& sim : mSimulations) { - MWWorld::Ptr standingOn; - auto* ptrHolder = static_cast(getUserPointer(actorData.mStandingOn)); - if (ptrHolder) - standingOn = ptrHolder->getPtr(); - actor.setStandingOnPtr(standingOn); - // the "on ground" state of an actor might have been updated by a traceDown, don't overwrite the change - if (actor.getOnGround() == actorData.mWasOnGround) - actor.setOnGround(actorData.mIsOnGround); - actor.setOnSlope(actorData.mIsOnSlope); - actor.setWalkingOnWater(actorData.mWalkingOnWater); - actor.setInertialForce(actorData.mInertia); + MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); + std::visit(vis, sim); } } @@ -532,10 +626,11 @@ namespace MWPhysics { mPreStepBarrier->wait([this] { afterPreStep(); }); int job = 0; + const Visitors::Move vis{mPhysicsDt, mCollisionWorld, *mWorldFrameData}; while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { MaybeLock lockColWorld(mCollisionWorldMutex, mNumThreads); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); + std::visit(vis, mSimulations[job]); } mPostStepBarrier->wait([this] { afterPostStep(); }); @@ -576,8 +671,9 @@ namespace MWPhysics void PhysicsTaskScheduler::releaseSharedStates() { + waitForWorkers(); std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); - mActors.clear(); + mSimulations.clear(); mUpdateAabb.clear(); } @@ -586,10 +682,11 @@ namespace MWPhysics updateAabbs(); if (!mRemainingSteps) return; - for (size_t i = 0; i < mActors.size(); ++i) + const Visitors::PreStep vis{mCollisionWorld}; + for (auto& sim : mSimulations) { MaybeExclusiveLock lock(mCollisionWorldMutex, mNumThreads); - MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); + std::visit(vis, sim); } } @@ -605,7 +702,6 @@ namespace MWPhysics void PhysicsTaskScheduler::afterPostSim() { - mNewFrame = false; { MaybeExclusiveLock lock(mLOSCacheMutex, mNumThreads); mLOSCache.erase( @@ -614,11 +710,31 @@ namespace MWPhysics mLOSCache.end()); } mTimeEnd = mTimer->tick(); + + std::unique_lock lock(mWorkersDoneMutex); + ++mWorkersFrameCounter; + mWorkersDone.notify_all(); } void PhysicsTaskScheduler::syncWithMainThread() { - for (size_t i = 0; i < mActors.size(); ++i) - updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); + const Visitors::Sync vis{mAdvanceSimulation, mTimeAccum, mPhysicsDt, this}; + for (auto& sim : mSimulations) + std::visit(vis, sim); + } + + // Attempt to acquire unique lock on mSimulationMutex while not all worker + // threads are holding shared lock but will have to may lead to a deadlock because + // C++ standard does not guarantee priority for exclusive and shared locks + // for std::shared_mutex. For example microsoft STL implementation points out + // for the absence of such priority: + // https://docs.microsoft.com/en-us/windows/win32/sync/slim-reader-writer--srw--locks + void PhysicsTaskScheduler::waitForWorkers() + { + if (mNumThreads == 0) + return; + std::unique_lock lock(mWorkersDoneMutex); + if (mFrameCounter != mWorkersFrameCounter) + mWorkersDone.wait(lock); } } diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 08997947e4..9ded1262d6 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -39,7 +40,7 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - void applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + void applyQueuedMovements(float & timeAccum, std::vector&& simulations, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); void resetSimulation(const ActorMap& actors); @@ -57,14 +58,12 @@ namespace MWPhysics bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); void debugDraw(); void* getUserPointer(const btCollisionObject* object) const; - void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: void doSimulation(); void worker(); void updateActorsPositions(); - void updateActor(Actor& actor, ActorFrameData& actorData, bool simulationPerformed, float timeAccum, float dt) const; bool hasLineOfSight(const Actor* actor1, const Actor* actor2); void refreshLOSCache(); void updateAabbs(); @@ -75,10 +74,10 @@ namespace MWPhysics void afterPostStep(); void afterPostSim(); void syncWithMainThread(); + void waitForWorkers(); std::unique_ptr mWorldFrameData; - std::vector> mActors; - std::vector mActorsFrameData; + std::vector mSimulations; std::unordered_set mCollisionObjects; float mDefaultPhysicsDt; float mPhysicsDt; @@ -97,13 +96,17 @@ namespace MWPhysics int mNumJobs; int mRemainingSteps; int mLOSCacheExpiry; - bool mNewFrame; + std::size_t mFrameCounter; bool mAdvanceSimulation; bool mQuit; std::atomic mNextJob; std::atomic mNextLOS; std::vector mThreads; + std::size_t mWorkersFrameCounter = 0; + std::condition_variable mWorkersDone; + std::mutex mWorkersDoneMutex; + mutable std::shared_mutex mSimulationMutex; mutable std::shared_mutex mCollisionWorldMutex; mutable std::shared_mutex mLOSCacheMutex; diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index a95672f8cf..08fcc7e47d 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -15,22 +16,18 @@ namespace MWPhysics { Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr shapeInstance, osg::Quat rotation, int collisionType, PhysicsTaskScheduler* scheduler) - : mShapeInstance(shapeInstance) + : mShapeInstance(std::move(shapeInstance)) , mSolid(true) + , mScale(ptr.getCellRef().getScale(), ptr.getCellRef().getScale(), ptr.getCellRef().getScale()) + , mPosition(ptr.getRefData().getPosition().asVec3()) + , mRotation(rotation) , mTaskScheduler(scheduler) { mPtr = ptr; - - mCollisionObject = std::make_unique(); - mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape()); - + mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), + Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); mCollisionObject->setUserPointer(this); - - setScale(ptr.getCellRef().getScale()); - setRotation(rotation); - updatePosition(); - commitPositionChange(); - + mShapeInstance->setLocalScaling(mScale); mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); } @@ -112,9 +109,9 @@ namespace MWPhysics if (mShapeInstance->mAnimatedShapes.empty()) return false; - assert (mShapeInstance->getCollisionShape()->isCompound()); + assert (mShapeInstance->mCollisionShape->isCompound()); - btCompoundShape* compound = static_cast(mShapeInstance->getCollisionShape()); + btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); diff --git a/apps/openmw/mwphysics/object.hpp b/apps/openmw/mwphysics/object.hpp index 1484c1472c..520a57a4e8 100644 --- a/apps/openmw/mwphysics/object.hpp +++ b/apps/openmw/mwphysics/object.hpp @@ -49,8 +49,8 @@ namespace MWPhysics btVector3 mScale; osg::Vec3f mPosition; osg::Quat mRotation; - bool mScaleUpdatePending; - bool mTransformUpdatePending; + bool mScaleUpdatePending = false; + bool mTransformUpdatePending = false; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; }; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 6b2600069a..98e3bcf737 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include // FindRecIndexVisitor @@ -61,19 +60,6 @@ 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); - } - void handleJump(const MWWorld::Ptr &ptr) { if (!ptr.getClass().isActor()) @@ -160,11 +146,6 @@ namespace MWPhysics mProjectiles.clear(); } - void PhysicsSystem::setUnrefQueue(SceneUtil::UnrefQueue *unrefQueue) - { - mUnrefQueue = unrefQueue; - } - Resource::BulletShapeManager *PhysicsSystem::getShapeManager() { return mShapeManager.get(); @@ -376,6 +357,8 @@ namespace MWPhysics bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { + if (actor1 == actor2) return true; + const auto it1 = mActors.find(actor1.mRef); const auto it2 = mActors.find(actor2.mRef); if (it1 == mActors.end() || it2 == mActors.end()) @@ -392,7 +375,8 @@ namespace MWPhysics bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr &actor, const float waterlevel) { - return ::canMoveToWaterSurface(getActor(actor), waterlevel, mCollisionWorld.get()); + const auto* physactor = getActor(actor); + return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr &actor) const @@ -472,9 +456,9 @@ namespace MWPhysics return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); } - void PhysicsSystem::addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject) + void PhysicsSystem::addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject) { - mHeightFields[std::make_pair(x,y)] = std::make_unique(heights, x, y, triSize, sqrtVerts, minH, maxH, holdObject, mTaskScheduler.get()); + mHeightFields[std::make_pair(x,y)] = std::make_unique(heights, x, y, size, verts, minH, maxH, holdObject, mTaskScheduler.get()); } void PhysicsSystem::removeHeightField (int x, int y) @@ -497,7 +481,7 @@ namespace MWPhysics if (ptr.mRef->mData.mPhysicsPostponed) return; osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); - if (!shapeInstance || !shapeInstance->getCollisionShape()) + if (!shapeInstance || !shapeInstance->mCollisionShape) return; assert(!getObject(ptr)); @@ -513,9 +497,6 @@ namespace MWPhysics { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(foundObject->second->getShapeInstance()); - mAnimatedObjects.erase(foundObject->second.get()); mObjects.erase(foundObject); @@ -535,10 +516,10 @@ namespace MWPhysics void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { - if (auto found = mObjects.find(old.mRef); found != mObjects.end()) - found->second->updatePtr(updated); - else if (auto found = mActors.find(old.mRef); found != mActors.end()) - found->second->updatePtr(updated); + if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) + foundObject->second->updatePtr(updated); + else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) + foundActor->second->updatePtr(updated); for (auto& [_, actor] : mActors) { @@ -601,33 +582,6 @@ namespace MWPhysics } } - void PhysicsSystem::updateProjectile(const int projectileId, const osg::Vec3f &position) const - { - const auto foundProjectile = mProjectiles.find(projectileId); - assert(foundProjectile != mProjectiles.end()); - auto* projectile = foundProjectile->second.get(); - - btVector3 btFrom = Misc::Convert::toBullet(projectile->getPosition()); - btVector3 btTo = Misc::Convert::toBullet(position); - - if (btFrom == btTo) - return; - - ProjectileConvexCallback resultCallback(projectile->getCasterCollisionObject(), projectile->getCollisionObject(), btFrom, btTo, projectile); - resultCallback.m_collisionFilterMask = 0xff; - resultCallback.m_collisionFilterGroup = CollisionType_Projectile; - - const btQuaternion btrot = btQuaternion::getIdentity(); - btTransform from_ (btrot, btFrom); - btTransform to_ (btrot, btTo); - - mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(projectile->getHitPosition()); - projectile->setPosition(newpos); - mTaskScheduler->updateSingleAabb(foundProjectile->second); - } - void PhysicsSystem::updateRotation(const MWWorld::Ptr &ptr, osg::Quat rotate) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) @@ -664,7 +618,7 @@ namespace MWPhysics osg::ref_ptr shape = mShapeManager->getShape(mesh); // Try to get shape from basic model as fallback for creatures - if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.extents.length2() == 0) + if (!ptr.getClass().isNpc() && shape && shape->mCollisionBox.mExtents.length2() == 0) { const std::string fallbackModel = ptr.getClass().getModel(ptr); if (fallbackModel != mesh) @@ -689,7 +643,7 @@ namespace MWPhysics { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); - float radius = computeRadius ? shapeInstance->mCollisionBox.extents.length() / 2.f : 1.f; + float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; mProjectileId++; @@ -736,11 +690,10 @@ namespace MWPhysics actor->setVelocity(osg::Vec3f()); } - std::pair>, std::vector> PhysicsSystem::prepareFrameData(bool willSimulate) + std::vector PhysicsSystem::prepareSimulation(bool willSimulate) { - std::pair>, std::vector> framedata; - framedata.first.reserve(mActors.size()); - framedata.second.reserve(mActors.size()); + std::vector simulations; + simulations.reserve(mActors.size() + mProjectiles.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); for (const auto& [ref, physicActor] : mActors) { @@ -765,18 +718,23 @@ namespace MWPhysics physicActor->setCanWaterWalk(waterCollision); // Slow fall reduces fall speed by a factor of (effect magnitude / 200) - const float slowFall = 1.f - std::max(0.f, std::min(1.f, effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f)); + const float slowFall = 1.f - std::clamp(effects.get(ESM::MagicEffect::SlowFall).getMagnitude() * 0.005f, 0.f, 1.f); const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - framedata.first.emplace_back(physicActor); - framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel); + simulations.emplace_back(ActorSimulation{physicActor, ActorFrameData{*physicActor, inert, waterCollision, slowFall, waterlevel}}); // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. if (willSimulate) handleJump(ptr); } - return framedata; + + for (const auto& [id, projectile] : mProjectiles) + { + simulations.emplace_back(ProjectileSimulation{projectile, ProjectileFrameData{*projectile}}); + } + + return simulations; } void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -802,9 +760,9 @@ namespace MWPhysics mTaskScheduler->resetSimulation(mActors); else { - auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt); + auto simulations = prepareSimulation(mTimeAccum >= mPhysicsDt); // modifies mTimeAccum - mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(actors), std::move(framedata), frameStart, frameNumber, stats); + mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(simulations), frameStart, frameNumber, stats); } } @@ -983,25 +941,17 @@ namespace MWPhysics , mWasOnGround(actor.getOnGround()) , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) , mWaterCollision(waterCollision) - , mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode()) + , mSkipCollisionDetection(!actor.getCollisionMode()) { } - void ActorFrameData::updatePosition(Actor& actor, btCollisionWorld* world) + ProjectileFrameData::ProjectileFrameData(Projectile& projectile) + : mPosition(projectile.getPosition()) + , mMovement(projectile.velocity()) + , mCaster(projectile.getCasterCollisionObject()) + , mCollisionObject(projectile.getCollisionObject()) + , mProjectile(&projectile) { - actor.applyOffsetChange(); - mPosition = actor.getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(&actor, mWaterlevel, world)) - { - mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false); - } - mOldHeight = mPosition.z(); - const auto rotation = actor.getPtr().getRefData().getPosition().asRotationVec3(); - mRotation = osg::Vec2f(rotation.x(), rotation.z()); - mInertia = actor.getInertialForce(); - mStuckFrames = actor.getStuckFrames(); - mLastStuckPosition = actor.getLastStuckPosition(); } WorldFrameData::WorldFrameData() diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index cf4ca40a54..c31bbfbf65 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -36,11 +37,6 @@ namespace Resource class ResourceSystem; } -namespace SceneUtil -{ - class UnrefQueue; -} - class btCollisionWorld; class btBroadphaseInterface; class btDefaultCollisionConfiguration; @@ -80,7 +76,6 @@ namespace MWPhysics struct ActorFrameData { ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel); - void updatePosition(Actor& actor, btCollisionWorld* world); osg::Vec3f mPosition; osg::Vec3f mInertia; const btCollisionObject* mStandingOn; @@ -105,6 +100,16 @@ namespace MWPhysics const bool mSkipCollisionDetection; }; + struct ProjectileFrameData + { + explicit ProjectileFrameData(Projectile& projectile); + osg::Vec3f mPosition; + osg::Vec3f mMovement; + const btCollisionObject* mCaster; + const btCollisionObject* mCollisionObject; + Projectile* mProjectile; + }; + struct WorldFrameData { WorldFrameData(); @@ -112,14 +117,16 @@ namespace MWPhysics osg::Vec3f mStormDirection; }; + using ActorSimulation = std::pair, ActorFrameData>; + using ProjectileSimulation = std::pair, ProjectileFrameData>; + using Simulation = std::variant; + class PhysicsSystem : public RayCastingInterface { public: PhysicsSystem (Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); virtual ~PhysicsSystem (); - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); - Resource::BulletShapeManager* getShapeManager(); void enableWater(float height); @@ -131,7 +138,6 @@ namespace MWPhysics int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); - void updateProjectile(const int projectileId, const osg::Vec3f &position) const; void removeProjectile(const int projectileId); void updatePtr (const MWWorld::Ptr& old, const MWWorld::Ptr& updated); @@ -150,7 +156,7 @@ namespace MWPhysics void updateRotation (const MWWorld::Ptr& ptr, osg::Quat rotate); void updatePosition (const MWWorld::Ptr& ptr); - void addHeightField (const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject); + void addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject); void removeHeightField (int x, int y); @@ -260,9 +266,7 @@ namespace MWPhysics void updateWater(); - std::pair>, std::vector> prepareFrameData(bool willSimulate); - - osg::ref_ptr mUnrefQueue; + std::vector prepareSimulation(bool willSimulate); std::unique_ptr mBroadphase; std::unique_ptr mCollisionConfiguration; diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index 4efb245149..9f8962d5e6 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -31,14 +31,15 @@ Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, f mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); - setPosition(position); + mPosition = position; + mPreviousPosition = position; setCaster(caster); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); - commitPositionChange(); + updateCollisionObjectPosition(); } Projectile::~Projectile() @@ -48,29 +49,12 @@ Projectile::~Projectile() mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } -void Projectile::commitPositionChange() -{ - std::scoped_lock lock(mMutex); - if (mTransformUpdatePending) - { - auto& trans = mCollisionObject->getWorldTransform(); - trans.setOrigin(Misc::Convert::toBullet(mPosition)); - mCollisionObject->setWorldTransform(trans); - mTransformUpdatePending = false; - } -} - -void Projectile::setPosition(const osg::Vec3f &position) -{ - std::scoped_lock lock(mMutex); - mPosition = position; - mTransformUpdatePending = true; -} - -osg::Vec3f Projectile::getPosition() const +void Projectile::updateCollisionObjectPosition() { std::scoped_lock lock(mMutex); - return mPosition; + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); } void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index 5e4e487c03..10ed2c9582 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -36,10 +36,7 @@ namespace MWPhysics btConvexShape* getConvexShape() const { return mConvexShape; } - void commitPositionChange(); - - void setPosition(const osg::Vec3f& position); - osg::Vec3f getPosition() const; + void updateCollisionObjectPosition(); bool isActive() const { @@ -80,13 +77,11 @@ namespace MWPhysics std::unique_ptr mShape; btConvexShape* mConvexShape; - bool mTransformUpdatePending; bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; const btCollisionObject* mCasterColObj; const btCollisionObject* mHitTarget; - osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index e84f3d1cfe..fcd6ce203a 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -30,9 +30,49 @@ namespace MWPhysics return mCollisionObject.get(); } + void setVelocity(osg::Vec3f velocity) + { + mVelocity = velocity; + } + + osg::Vec3f velocity() + { + return std::exchange(mVelocity, osg::Vec3f()); + } + + void setSimulationPosition(const osg::Vec3f& position) + { + mSimulationPosition = position; + } + + osg::Vec3f getSimulationPosition() const + { + return mSimulationPosition; + } + + void setPosition(const osg::Vec3f& position) + { + mPreviousPosition = mPosition; + mPosition = position; + } + + osg::Vec3f getPosition() const + { + return mPosition; + } + + osg::Vec3f getPreviousPosition() const + { + return mPreviousPosition; + } + protected: MWWorld::Ptr mPtr; std::unique_ptr mCollisionObject; + osg::Vec3f mVelocity; + osg::Vec3f mSimulationPosition; + osg::Vec3f mPosition; + osg::Vec3f mPreviousPosition; }; } diff --git a/apps/openmw/mwphysics/stepper.cpp b/apps/openmw/mwphysics/stepper.cpp index 1f53c1ac51..5ef6833701 100644 --- a/apps/openmw/mwphysics/stepper.cpp +++ b/apps/openmw/mwphysics/stepper.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "collisiontype.hpp" #include "constants.hpp" #include "movementsolver.hpp" @@ -13,7 +15,7 @@ namespace MWPhysics { if (!stepper.mHitObject) return false; - static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope)); + static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(Constants::sMaxSlope)); if (stepper.mPlaneNormal.z() <= sMaxSlopeCos) return false; @@ -34,13 +36,13 @@ namespace MWPhysics // Stairstepping algorithms work by moving up to avoid the step, moving forwards, then moving back down onto the ground. // This algorithm has a couple of minor problems, but they don't cause problems for sane geometry, and just prevent stepping on insane geometry. - mUpStepper.doTrace(mColObj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), mColWorld); + mUpStepper.doTrace(mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); float upDistance = 0; if(!mUpStepper.mHitObject) - upDistance = sStepSizeUp; - else if(mUpStepper.mFraction*sStepSizeUp > sCollisionMargin) - upDistance = mUpStepper.mFraction*sStepSizeUp - sCollisionMargin; + upDistance = Constants::sStepSizeUp; + else if(mUpStepper.mFraction * Constants::sStepSizeUp > sCollisionMargin) + upDistance = mUpStepper.mFraction * Constants::sStepSizeUp - sCollisionMargin; else { return false; @@ -76,9 +78,9 @@ namespace MWPhysics } else if(attempt == 3) { - if(upDistance > sStepSizeUp) + if(upDistance > Constants::sStepSizeUp) { - upDistance = sStepSizeUp; + upDistance = Constants::sStepSizeUp; tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); } moveDistance = sMinStep2; @@ -115,7 +117,7 @@ namespace MWPhysics downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; - mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld); + mDownStepper.doTrace(mColObj, tracerDest, tracerDest + osg::Vec3f(0.0f, 0.0f, -downStepSize), mColWorld, onGround); // can't step down onto air, non-walkable-slopes, or actors // NOTE: using a capsule causes isWalkableSlope (used in canStepDown) to fail on certain geometry that were intended to be valid at the bottoms of stairs diff --git a/apps/openmw/mwphysics/trace.cpp b/apps/openmw/mwphysics/trace.cpp index 049d026e8e..b7930bfa53 100644 --- a/apps/openmw/mwphysics/trace.cpp +++ b/apps/openmw/mwphysics/trace.cpp @@ -12,38 +12,84 @@ namespace MWPhysics { -void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) +ActorConvexCallback sweepHelper(const btCollisionObject *actor, const btVector3& from, const btVector3& to, const btCollisionWorld* world, bool actorFilter) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - const btTransform &trans = actor->getWorldTransform(); - btTransform from(trans); - btTransform to(trans); - from.setOrigin(btstart); - to.setOrigin(btend); - - const btVector3 motion = btstart-btend; - ActorConvexCallback newTraceCallback(actor, motion, btScalar(0.0), world); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + btTransform transFrom(trans); + btTransform transTo(trans); + transFrom.setOrigin(from); + transTo.setOrigin(to); const btCollisionShape *shape = actor->getCollisionShape(); assert(shape->isConvex()); - world->convexSweepTest(static_cast(shape), from, to, newTraceCallback); + + const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too + ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); + // Inherit the actor's collision group and mask + traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; + traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; + if(actorFilter) + traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; + + world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); + return traceCallback; +} + +void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace) +{ + const btVector3 btstart = Misc::Convert::toBullet(start); + btVector3 btend = Misc::Convert::toBullet(end); + + // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests + // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be + // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. + // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. + // This trace needs to be at least a couple units long, but there's no one particular ideal length. + // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. + // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) + const float fallback_length = 2.1f; + bool doing_short_trace = false; + // For some reason, typical scenes perform a little better if we increase the threshold length for the length test. + // (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks this was + // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) + if(attempt_short_trace && (btend-btstart).length2() > fallback_length*fallback_length*2.0) + { + btend = btstart + (btend-btstart).normalized()*fallback_length; + doing_short_trace = true; + } + + const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: - if(newTraceCallback.hasHit()) + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + // ensure fraction is correct (covers intended distance traveled instead of actual distance traveled) + if(doing_short_trace && (end-start).length2() > 0.0) + mFraction *= (btend-btstart).length() / (end-start).length(); + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; - mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); - mHitObject = newTraceCallback.m_hitCollisionObject; + mHitPoint = Misc::Convert::toOsg(traceCallback.m_hitPointWorld); + mHitObject = traceCallback.m_hitCollisionObject; } else { + if(doing_short_trace) + { + btend = Misc::Convert::toBullet(end); + const auto newTraceCallback = sweepHelper(actor, btstart, btend, world, false); + + if(newTraceCallback.hasHit()) + { + mFraction = newTraceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mEndPos = (end-start)*mFraction + start; + mHitPoint = Misc::Convert::toOsg(newTraceCallback.m_hitPointWorld); + mHitObject = newTraceCallback.m_hitCollisionObject; + return; + } + } + // fallthrough mEndPos = end; mPlaneNormal = osg::Vec3f(0.0f, 0.0f, 1.0f); mFraction = 1.0f; @@ -54,25 +100,11 @@ void ActorTracer::doTrace(const btCollisionObject *actor, const osg::Vec3f& star void ActorTracer::findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { - const btVector3 btstart = Misc::Convert::toBullet(start); - const btVector3 btend = Misc::Convert::toBullet(end); - - const btTransform &trans = actor->getCollisionObject()->getWorldTransform(); - btTransform from(trans.getBasis(), btstart); - btTransform to(trans.getBasis(), btend); - - const btVector3 motion = btstart-btend; - ActorConvexCallback newTraceCallback(actor->getCollisionObject(), motion, btScalar(0.0), world); - // Inherit the actor's collision group and mask - newTraceCallback.m_collisionFilterGroup = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterGroup; - newTraceCallback.m_collisionFilterMask = actor->getCollisionObject()->getBroadphaseHandle()->m_collisionFilterMask; - newTraceCallback.m_collisionFilterMask &= ~CollisionType_Actor; - - world->convexSweepTest(actor->getConvexShape(), from, to, newTraceCallback); - if(newTraceCallback.hasHit()) + const auto traceCallback = sweepHelper(actor->getCollisionObject(), Misc::Convert::toBullet(start), Misc::Convert::toBullet(end), world, true); + if(traceCallback.hasHit()) { - mFraction = newTraceCallback.m_closestHitFraction; - mPlaneNormal = Misc::Convert::toOsg(newTraceCallback.m_hitNormalWorld); + mFraction = traceCallback.m_closestHitFraction; + mPlaneNormal = Misc::Convert::toOsg(traceCallback.m_hitNormalWorld); mEndPos = (end-start)*mFraction + start; } else diff --git a/apps/openmw/mwphysics/trace.h b/apps/openmw/mwphysics/trace.h index 0297c9e076..af38756b3e 100644 --- a/apps/openmw/mwphysics/trace.h +++ b/apps/openmw/mwphysics/trace.h @@ -20,7 +20,7 @@ namespace MWPhysics float mFraction; - void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); + void doTrace(const btCollisionObject *actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace = false); void findGround(const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world); }; } diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index f556e6891a..25a9904e26 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -74,7 +75,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return PartHolderPtr(); @@ -84,30 +85,58 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } -std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +osg::ref_ptr ActorAnimation::attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight) +{ + osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); + + const NodeMap& nodeMap = getNodeMap(); + auto found = nodeMap.find(bonename); + if (found == nodeMap.end()) + throw std::runtime_error("Can't find attachment node " + bonename); + if(isLight) + { + osg::Quat rotation(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0)); + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager(), &rotation); + } + return SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second, mResourceSystem->getSceneManager()); +} + +std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const { - std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; const std::vector& bodyparts = armor->mParts.mParts; + // Try to recover the body part model, use ground model as a fallback otherwise. if (!bodyparts.empty()) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::Store &partStore = store.get(); - - // Try to get shield model from bodyparts first, with ground model as fallback for (const auto& part : bodyparts) { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) + if (part.mPart != ESM::PRT_Shield) continue; - const ESM::BodyPart *bodypart = partStore.search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) + + std::string bodypartName; + if (female && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) { - mesh = "meshes\\" + bodypart->mModel; - break; + const ESM::BodyPart *bodypart = partStore.search(bodypartName); + if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return std::string(); + if (!bodypart->mModel.empty()) + return "meshes\\" + bodypart->mModel; } } } + return shield.getClass().getModel(shield); +} + +std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const +{ + std::string mesh = getShieldMesh(shield, false); if (mesh.empty()) return mesh; @@ -143,21 +172,21 @@ bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) + if (shield != inv.end() && shield->getType() == ESM::Armor::sRecordId && !getSheathedShieldMesh(*shield).empty()) { if(stats.getDrawState() != MWMechanics::DrawState_Weapon) return false; if (weapon != inv.end()) { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) return true; } } @@ -184,7 +213,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + if (shield == inv.end() || shield->getType() != ESM::Armor::sRecordId) return; // Can not show holdstered shields with two-handed weapons at all @@ -192,8 +221,8 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) if(weapon == inv.end()) return; - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; @@ -201,7 +230,7 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) return; } - std::string mesh = getShieldMesh(*shield); + std::string mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; @@ -232,9 +261,6 @@ void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) if (isEnchanted) SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); } - - if (mAlpha != 1.f) - mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); } bool ActorAnimation::useShieldAnimations() const @@ -254,17 +280,17 @@ bool ActorAnimation::useShieldAnimations() const const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (weapon != inv.end() && shield != inv.end() && - shield->getTypeName() == typeid(ESM::Armor).name() && - !getShieldMesh(*shield).empty()) + shield->getType() == ESM::Armor::sRecordId && + !getSheathedShieldMesh(*shield).empty()) { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon->getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon->get(); ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); } - else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) return true; } @@ -288,8 +314,8 @@ std::string ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& if(weapon.isEmpty()) return boneName; - const std::string &type = weapon.getClass().getTypeName(); - if(type == typeid(ESM::Weapon).name()) + auto type = weapon.getClass().getType(); + if(type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef *ref = weapon.get(); int weaponType = ref->mBase->mData.mType; @@ -306,7 +332,7 @@ void ActorAnimation::resetControllers(osg::Node* node) std::shared_ptr src; src.reset(new NullAnimationTime); - SceneUtil::AssignControllerSourcesVisitor removeVisitor(src); + SceneUtil::ForceControllerSourcesVisitor removeVisitor(src); node->accept(removeVisitor); } @@ -323,7 +349,7 @@ void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if (weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if (weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; // Since throwing weapons stack themselves, do not show such weapon itself @@ -399,7 +425,7 @@ void ActorAnimation::updateQuiver() const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; std::string mesh = weapon->getClass().getModel(*weapon); @@ -471,7 +497,7 @@ void ActorAnimation::updateQuiver() void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) { - if (item.getTypeName() == typeid(ESM::Light).name()) + if (item.getType() == ESM::Light::sRecordId) { const ESM::Light* light = item.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) @@ -486,7 +512,7 @@ void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); @@ -502,7 +528,7 @@ void ActorAnimation::itemAdded(const MWWorld::ConstPtr& item, int /*count*/) void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) { - if (item.getTypeName() == typeid(ESM::Light).name()) + if (item.getType() == ESM::Light::sRecordId) { ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) @@ -520,7 +546,7 @@ void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) // If the count of equipped ammo or throwing weapon was changed, we should update quiver const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return; MWWorld::ConstContainerStoreIterator ammo = inv.end(); diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 61ad1ca235..1ece0c326d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -45,7 +45,8 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(const MWWorld::ConstPtr& shield) const; + std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; + virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) @@ -53,6 +54,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener osg::Vec4f stubColor = osg::Vec4f(0,0,0,0); return attachMesh(model, bonename, false, &stubColor); }; + osg::ref_ptr attach(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp index 4e3bfd79a6..941f37df75 100644 --- a/apps/openmw/mwrender/actorspaths.cpp +++ b/apps/openmw/mwrender/actorspaths.cpp @@ -9,6 +9,9 @@ #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" + +#include + namespace MWRender { ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) diff --git a/apps/openmw/mwrender/actorspaths.hpp b/apps/openmw/mwrender/actorspaths.hpp index 1f61834d46..12f102093b 100644 --- a/apps/openmw/mwrender/actorspaths.hpp +++ b/apps/openmw/mwrender/actorspaths.hpp @@ -3,18 +3,22 @@ #include -#include - #include #include #include +#include namespace osg { class Group; } +namespace DetourNavigator +{ + struct Settings; +} + namespace MWRender { class ActorsPaths diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 475c656cc9..bf0ed04055 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -389,11 +389,6 @@ namespace MWRender mAlpha = alpha; } - void setLightSource(const osg::ref_ptr& lightSource) - { - mLightSource = lightSource; - } - protected: void setDefaults(osg::StateSet* stateset) override { @@ -416,13 +411,10 @@ 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 @@ -632,9 +624,8 @@ namespace MWRender return; const NodeMap& nodeMap = getNodeMap(); - - for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = animsrc->mKeyframes->mKeyframeControllers.begin(); - it != animsrc->mKeyframes->mKeyframeControllers.end(); ++it) + const auto& controllerMap = animsrc->mKeyframes->mKeyframeControllers; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it) { std::string bonename = Misc::StringUtils::lowerCase(it->first); NodeMap::const_iterator found = nodeMap.find(bonename); @@ -660,14 +651,32 @@ namespace MWRender SceneUtil::AssignControllerSourcesVisitor assignVisitor(mAnimationTimePtr[0]); mObjectRoot->accept(assignVisitor); + // Determine the movement accumulation bone if necessary if (!mAccumRoot) { - NodeMap::const_iterator found = nodeMap.find("bip01"); - if (found == nodeMap.end()) - found = nodeMap.find("root bone"); - - if (found != nodeMap.end()) - mAccumRoot = found->second; + // Priority matters! bip01 is preferred. + static const std::array accumRootNames = + { + "bip01", + "root bone" + }; + NodeMap::const_iterator found = nodeMap.end(); + for (const std::string& name : accumRootNames) + { + found = nodeMap.find(name); + if (found == nodeMap.end()) + continue; + for (SceneUtil::KeyframeHolder::KeyframeControllerMap::const_iterator it = controllerMap.begin(); it != controllerMap.end(); ++it) + { + if (Misc::StringUtils::lowerCase(it->first) == name) + { + mAccumRoot = found->second; + break; + } + } + if (mAccumRoot) + break; + } } } @@ -968,8 +977,9 @@ namespace MWRender { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource - node->addUpdateCallback(it->second); - mActiveControllers.emplace_back(node, it->second); + osg::Callback* callback = it->second->getAsCallback(); + node->addUpdateCallback(callback); + mActiveControllers.emplace_back(node, callback); if (blendMask == 0 && node == mAccumRoot) { @@ -1319,10 +1329,10 @@ namespace MWRender cache.insert(std::make_pair(model, created)); - return sceneMgr->createInstance(created); + return sceneMgr->getInstance(created); } else - return sceneMgr->createInstance(found->second); + return sceneMgr->getInstance(found->second); } else { @@ -1484,6 +1494,7 @@ namespace MWRender bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); mExtraLightSource = SceneUtil::addLight(parent, esmLight, Mask_ParticleSystem, Mask_Lighting, exterior); + mExtraLightSource->setActorFade(mAlpha); } void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) @@ -1510,7 +1521,7 @@ namespace MWRender parentNode = mInsert; else { - NodeMap::const_iterator found = getNodeMap().find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + bonename); @@ -1619,8 +1630,7 @@ namespace MWRender const osg::Node* Animation::getNode(const std::string &name) const { - std::string lowerName = Misc::StringUtils::lowerCase(name); - NodeMap::const_iterator found = getNodeMap().find(lowerName); + NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else @@ -1639,7 +1649,6 @@ namespace MWRender if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); - mTransparencyUpdater->setLightSource(mExtraLightSource); mObjectRoot->addCullCallback(mTransparencyUpdater); } else @@ -1650,6 +1659,8 @@ namespace MWRender mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } + if (mExtraLightSource) + mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) @@ -1794,7 +1805,7 @@ namespace MWRender if (!ptr.getClass().getEnchantment(ptr).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); } - if (ptr.getTypeName() == typeid(ESM::Light).name() && allowLight) + if (ptr.getType() == ESM::Light::sRecordId && allowLight) addExtraLight(getOrCreateObjectRoot(), ptr.get()->mBase); if (!allowLight && mObjectRoot) @@ -1823,7 +1834,7 @@ namespace MWRender bool ObjectAnimation::canBeHarvested() const { - if (mPtr.getTypeName() != typeid(ESM::Container).name()) + if (mPtr.getType() != ESM::Container::sRecordId) return false; const MWWorld::LiveCellRef* ref = mPtr.get(); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 84788c1e2d..d37d548dd3 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -7,8 +7,10 @@ #include #include #include +#include #include +#include namespace ESM { @@ -157,6 +159,8 @@ public: virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -250,8 +254,6 @@ protected: std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; - // Stored in all lowercase for a case-insensitive lookup - typedef std::map > NodeMap; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp index 9155132871..b169251465 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.cpp +++ b/apps/openmw/mwrender/bulletdebugdraw.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -66,7 +66,7 @@ void DebugDrawer::createGeometry() auto* stateSet = new osg::StateSet; stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); - stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::getReverseZ() ? 1.0 : -1.0, SceneUtil::getReverseZ() ? 1.0 : -1.0)); + stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0, SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0)); osg::ref_ptr material = new osg::Material; material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); stateSet->setAttribute(material); diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index e750fcad46..5ca102bc39 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -56,41 +56,25 @@ namespace MWRender mCamera(camera), mAnimation(nullptr), mFirstPersonView(true), - mMode(Mode::Normal), + mMode(Mode::FirstPerson), mVanityAllowed(true), - mStandingPreviewAllowed(Settings::Manager::getBool("preview if stand still", "Camera")), - mDeferredRotationAllowed(Settings::Manager::getBool("deferred preview rotation", "Camera")), - mNearest(30.f), - mFurthest(800.f), - mIsNearest(false), + mDeferredRotationAllowed(true), + mProcessViewChange(false), mHeight(124.f), - mBaseCameraDistance(Settings::Manager::getFloat("third person camera distance", "Camera")), mPitch(0.f), mYaw(0.f), mRoll(0.f), - mVanityToggleQueued(false), - mVanityToggleQueuedValue(false), - mViewModeToggleQueued(false), mCameraDistance(0.f), - mMaxNextCameraDistance(800.f), + mPreferredCameraDistance(0.f), mFocalPointCurrentOffset(osg::Vec2d()), mFocalPointTargetOffset(osg::Vec2d()), mFocalPointTransitionSpeedCoef(1.f), mSkipFocalPointTransition(true), mPreviousTransitionInfluence(0.f), - mSmoothedSpeed(0.f), - mZoomOutWhenMoveCoef(Settings::Manager::getFloat("zoom out when move coef", "Camera")), - mDynamicCameraDistanceEnabled(false), - mShowCrosshairInThirdPersonMode(false), - mHeadBobbingEnabled(Settings::Manager::getBool("head bobbing", "Camera")), - mHeadBobbingOffset(0.f), - mHeadBobbingWeight(0.f), - mTotalMovement(0.f), + mShowCrosshair(false), mDeferredRotation(osg::Vec3f()), mDeferredRotationDisabled(false) { - mCameraDistance = mBaseCameraDistance; - mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } @@ -100,7 +84,7 @@ namespace MWRender mCamera->removeUpdateCallback(mUpdateCallback); } - osg::Vec3d Camera::getFocalPoint() const + osg::Vec3d Camera::calculateTrackedPosition() const { if (!mTrackingNode) return osg::Vec3d(); @@ -108,155 +92,95 @@ namespace MWRender if (nodepaths.empty()) return osg::Vec3d(); osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); - - osg::Vec3d position = worldMat.getTrans(); - if (isFirstPerson()) - position.z() += mHeadBobbingOffset; - else - { - position.z() += mHeight * mHeightScale; - - // We subtract 10.f here and add it within focalPointOffset in order to avoid camera clipping through ceiling. - // Needed because character's head can be a bit higher than collision area. - position.z() -= 10.f; - - position += getFocalPointOffset() + mFocalPointAdjustment; - } - return position; + osg::Vec3d res = worldMat.getTrans(); + if (mMode != Mode::FirstPerson) + res.z() += mHeight * mHeightScale; + return res; } osg::Vec3d Camera::getFocalPointOffset() const { - osg::Vec3d offset(0, 0, 10.f); - offset.x() += mFocalPointCurrentOffset.x() * cos(getYaw()); - offset.y() += mFocalPointCurrentOffset.x() * sin(getYaw()); - offset.z() += mFocalPointCurrentOffset.y(); + osg::Vec3d offset; + offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw); + offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw); + offset.z() = mFocalPointCurrentOffset.y(); return offset; } - void Camera::getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const - { - focal = getFocalPoint(); - osg::Vec3d offset(0,0,0); - if (!isFirstPerson()) - { - osg::Quat orient = osg::Quat(getPitch(), osg::Vec3d(1,0,0)) * osg::Quat(getYaw(), osg::Vec3d(0,0,1)); - offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); - } - camera = focal + offset; - } - void Camera::updateCamera(osg::Camera *cam) { - osg::Vec3d focal, position; - getPosition(focal, position); - - osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); + osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * + osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1, 0, 0)) * + osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0, 0, 1)); osg::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); - cam->setViewMatrixAsLookAt(position, position + forward, up); - } - - void Camera::updateHeadBobbing(float duration) { - static const float doubleStepLength = Settings::Manager::getFloat("head bobbing step", "Camera") * 2; - static const float stepHeight = Settings::Manager::getFloat("head bobbing height", "Camera"); - static const float maxRoll = osg::DegreesToRadians(Settings::Manager::getFloat("head bobbing roll", "Camera")); - - if (MWBase::Environment::get().getWorld()->isOnGround(mTrackingPtr)) - mHeadBobbingWeight = std::min(mHeadBobbingWeight + duration * 5, 1.f); - else - mHeadBobbingWeight = std::max(mHeadBobbingWeight - duration * 5, 0.f); - - float doubleStepState = mTotalMovement / doubleStepLength - std::floor(mTotalMovement / doubleStepLength); // from 0 to 1 during 2 steps - float stepState = std::abs(doubleStepState * 4 - 2) - 1; // from -1 to 1 on even steps and from 1 to -1 on odd steps - float effect = (1 - std::cos(stepState * osg::DegreesToRadians(30.f))) * 7.5f; // range from 0 to 1 - float coef = std::min(mSmoothedSpeed / 300.f, 1.f) * mHeadBobbingWeight; - mHeadBobbingOffset = (0.5f - effect) * coef * stepHeight; // range from -stepHeight/2 to stepHeight/2 - mRoll = osg::sign(stepState) * effect * coef * maxRoll; // range from -maxRoll to maxRoll - } - - void Camera::reset() - { - togglePreviewMode(false); - toggleVanityMode(false); - if (!mFirstPersonView) - toggleViewMode(); - } - - void Camera::rotateCamera(float pitch, float yaw, bool adjust) - { - if (adjust) + osg::Vec3d pos = mPosition; + if (mMode == Mode::FirstPerson) { - pitch += getPitch(); - yaw += getYaw(); + // It is a hack. Camera position depends on neck animation. + // Animations are updated in OSG cull traversal and in order to avoid 1 frame delay we + // recalculate the position here. Note that it becomes different from mPosition that + // is used in other parts of the code. + // TODO: detach camera from OSG animation and get rid of this hack. + osg::Vec3d recalculatedTrackedPosition = calculateTrackedPosition(); + pos = calculateFirstPersonPosition(recalculatedTrackedPosition); } - setYaw(yaw); - setPitch(pitch); + cam->setViewMatrixAsLookAt(pos, pos + forward, up); } void Camera::update(float duration, bool paused) { - if (mAnimation->upperBodyReady()) - { - // Now process the view changes we queued earlier - if (mVanityToggleQueued) - { - toggleVanityMode(mVanityToggleQueuedValue); - mVanityToggleQueued = false; - } - if (mViewModeToggleQueued) - { - togglePreviewMode(false); - toggleViewMode(); - mViewModeToggleQueued = false; - } - } + mLockPitch = mLockYaw = false; + if (mQueuedMode && mAnimation->upperBodyReady()) + setMode(*mQueuedMode); + if (mProcessViewChange) + processViewChange(); if (paused) return; // only show the crosshair in game mode MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); - wm->showCrosshair(!wm->isGuiMode() && mMode != Mode::Preview && mMode != Mode::Vanity - && (mFirstPersonView || mShowCrosshairInThirdPersonMode)); - - if(mMode == Mode::Vanity) - rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); - - if (isFirstPerson() && mHeadBobbingEnabled) - updateHeadBobbing(duration); - else - mRoll = mHeadBobbingOffset = 0; + wm->showCrosshair(!wm->isGuiMode() && mShowCrosshair); updateFocalPointOffset(duration); updatePosition(); + } - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - mTotalMovement += speed * duration; - speed /= (1.f + speed / 500.f); - float maxDelta = 300.f * duration; - mSmoothedSpeed += osg::clampBetween(speed - mSmoothedSpeed, -maxDelta, maxDelta); - - mMaxNextCameraDistance = mCameraDistance + duration * (100.f + mBaseCameraDistance); - updateStandingPreviewMode(); + osg::Vec3d Camera::calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const + { + osg::Vec3d res = trackedPosition; + osg::Vec2f horizontalOffset = Misc::rotateVec2f(osg::Vec2f(mFirstPersonOffset.x(), mFirstPersonOffset.y()), mYaw); + res.x() += horizontalOffset.x(); + res.y() += horizontalOffset.y(); + res.z() += mFirstPersonOffset.z(); + return res; } void Camera::updatePosition() { - mFocalPointAdjustment = osg::Vec3d(); - if (isFirstPerson()) + mTrackedPosition = calculateTrackedPosition(); + if (mMode == Mode::Static) return; + if (mMode == Mode::FirstPerson) + { + mPosition = calculateFirstPersonPosition(mTrackedPosition); + mCameraDistance = 0; + return; + } - const float cameraObstacleLimit = 5.0f; - const float focalObstacleLimit = 10.f; - const int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor); + constexpr float cameraObstacleLimit = 5.0f; + constexpr float focalObstacleLimit = 10.f; const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + constexpr int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor); // Adjust focal point to prevent clipping. - osg::Vec3d focal = getFocalPoint(); osg::Vec3d focalOffset = getFocalPointOffset(); + osg::Vec3d focal = mTrackedPosition + focalOffset; + focalOffset.z() += 10.f; // Needed to avoid camera clipping through the ceiling because + // character's head can be a bit higher than the collision area. float offsetLen = focalOffset.length(); if (offsetLen > 0) { @@ -264,39 +188,51 @@ namespace MWRender if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; - mFocalPointAdjustment = focalOffset * std::max(-1.0, adjustmentCoef); + focal += focalOffset * std::max(-1.0, adjustmentCoef); } } - // Calculate camera distance. - mCameraDistance = mBaseCameraDistance + getCameraDistanceCorrection(); - if (mDynamicCameraDistanceEnabled) - mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); - osg::Vec3d cameraPos; - getPosition(focal, cameraPos); - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit, collisionType); + // Adjust camera distance. + mCameraDistance = mPreferredCameraDistance; + osg::Quat orient = osg::Quat(mPitch + mExtraPitch, osg::Vec3d(1,0,0)) * osg::Quat(mYaw + mExtraYaw, osg::Vec3d(0,0,1)); + osg::Vec3d offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, focal + offset, cameraObstacleLimit, collisionType); if (result.mHit) + { mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); + offset = orient * osg::Vec3d(0.f, -mCameraDistance, 0.f); + } + + mPosition = focal + offset; } - void Camera::updateStandingPreviewMode() + void Camera::setMode(Mode newMode, bool force) { - if (!mStandingPreviewAllowed) + if (mMode == newMode) return; - float speed = mTrackingPtr.getClass().getCurrentSpeed(mTrackingPtr); - bool combat = mTrackingPtr.getClass().isActor() && - mTrackingPtr.getClass().getCreatureStats(mTrackingPtr).getDrawState() != MWMechanics::DrawState_Nothing; - bool standingStill = speed == 0 && !combat && !mFirstPersonView; - if (!standingStill && mMode == Mode::StandingPreview) + Mode oldMode = mMode; + if (!force && (newMode == Mode::FirstPerson || oldMode == Mode::FirstPerson) && !mAnimation->upperBodyReady()) { - mMode = Mode::Normal; - calculateDeferredRotation(); + // Changing the view will stop all playing animations, so if we are playing + // anything important, queue the view change for later + mQueuedMode = newMode; + return; + } + mMode = newMode; + mQueuedMode = std::nullopt; + if (newMode == Mode::FirstPerson) + mFirstPersonView = true; + else if (newMode == Mode::ThirdPerson) + mFirstPersonView = false; + calculateDeferredRotation(); + if (oldMode == Mode::FirstPerson || newMode == Mode::FirstPerson) + { + instantTransition(); + mProcessViewChange = true; } - else if (standingStill && mMode == Mode::Normal) - mMode = Mode::StandingPreview; } - void Camera::setFocalPointTargetOffset(osg::Vec2d v) + void Camera::setFocalPointTargetOffset(const osg::Vec2d& v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; @@ -346,78 +282,16 @@ namespace MWRender void Camera::toggleViewMode(bool force) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (!mAnimation->upperBodyReady() && !force) - { - mViewModeToggleQueued = true; - return; - } - else - mViewModeToggleQueued = false; - - mFirstPersonView = !mFirstPersonView; - updateStandingPreviewMode(); - instantTransition(); - processViewChange(); - } - - void Camera::allowVanityMode(bool allow) - { - if (!allow && mMode == Mode::Vanity) - { - disableDeferredPreviewRotation(); - toggleVanityMode(false); - } - mVanityAllowed = allow; + setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force); } bool Camera::toggleVanityMode(bool enable) { - // Changing the view will stop all playing animations, so if we are playing - // anything important, queue the view change for later - if (mFirstPersonView && !mAnimation->upperBodyReady()) - { - mVanityToggleQueued = true; - mVanityToggleQueuedValue = enable; - return false; - } - - if (!mVanityAllowed && enable) - return false; - - if ((mMode == Mode::Vanity) == enable) - return true; - mMode = enable ? Mode::Vanity : Mode::Normal; - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); if (!enable) - calculateDeferredRotation(); - - processViewChange(); - return true; - } - - void Camera::togglePreviewMode(bool enable) - { - if (mFirstPersonView && !mAnimation->upperBodyReady()) - return; - - if((mMode == Mode::Preview) == enable) - return; - - mMode = enable ? Mode::Preview : Mode::Normal; - if (mMode == Mode::Normal) - updateStandingPreviewMode(); - else if (mFirstPersonView) - instantTransition(); - if (mMode == Mode::Normal) - { - if (!mDeferredRotationAllowed) - disableDeferredPreviewRotation(); - calculateDeferredRotation(); - } - processViewChange(); + setMode(mFirstPersonView ? Mode::FirstPerson : Mode::ThirdPerson, false); + else if (mVanityAllowed) + setMode(Mode::Vanity, false); + return (mMode == Mode::Vanity) == enable; } void Camera::setSneakOffset(float offset) @@ -425,67 +299,42 @@ namespace MWRender mAnimation->setFirstPersonOffset(osg::Vec3f(0,0,-offset)); } - void Camera::setYaw(float angle) + void Camera::setYaw(float angle, bool force) { - mYaw = Misc::normalizeAngle(angle); + if (!mLockYaw || force) + mYaw = Misc::normalizeAngle(angle); + if (force) + mLockYaw = true; } - void Camera::setPitch(float angle) + void Camera::setPitch(float angle, bool force) { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; - mPitch = osg::clampBetween(angle, -limit, limit); - } - - float Camera::getCameraDistance() const - { - if (isFirstPerson()) - return 0.f; - return mCameraDistance; + if (!mLockPitch || force) + mPitch = std::clamp(angle, -limit, limit); + if (force) + mLockPitch = true; } - void Camera::adjustCameraDistance(float delta) + void Camera::setStaticPosition(const osg::Vec3d& pos) { - if (!isFirstPerson()) - { - if(isNearest() && delta < 0.f && getMode() != Mode::Preview && getMode() != Mode::Vanity) - toggleViewMode(); - else - mBaseCameraDistance = std::min(mCameraDistance - getCameraDistanceCorrection(), mBaseCameraDistance) + delta; - } - else if (delta > 0.f) - { - toggleViewMode(); - mBaseCameraDistance = 0; - } - - mIsNearest = mBaseCameraDistance <= mNearest; - mBaseCameraDistance = osg::clampBetween(mBaseCameraDistance, mNearest, mFurthest); - Settings::Manager::setFloat("third person camera distance", "Camera", mBaseCameraDistance); - } - - float Camera::getCameraDistanceCorrection() const - { - if (!mDynamicCameraDistanceEnabled) - return 0; - - float pitchCorrection = std::max(-getPitch(), 0.f) * 50.f; - - float smoothedSpeedSqr = mSmoothedSpeed * mSmoothedSpeed; - float speedCorrection = smoothedSpeedSqr / (smoothedSpeedSqr + 300.f*300.f) * mZoomOutWhenMoveCoef; - - return pitchCorrection + speedCorrection; + if (mMode != Mode::Static) + throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode"); + mPosition = pos; } void Camera::setAnimation(NpcAnimation *anim) { mAnimation = anim; - processViewChange(); + mProcessViewChange = true; } void Camera::processViewChange() { - if(isFirstPerson()) + if (mTrackingPtr.isEmpty()) + return; + if (mMode == Mode::FirstPerson) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); @@ -503,12 +352,12 @@ namespace MWRender else mHeightScale = 1.f; } - rotateCamera(getPitch(), getYaw(), false); + mProcessViewChange = false; } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { - if (isVanityOrPreviewModeEnabled() || mTrackingPtr.isEmpty()) + if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; @@ -541,6 +390,8 @@ namespace MWRender void Camera::rotateCameraToTrackingPtr() { + if (mMode == Mode::Static || mTrackingPtr.isEmpty()) + return; setPitch(-mTrackingPtr.getRefData().getPosition().rot[0] - mDeferredRotation.x()); setYaw(-mTrackingPtr.getRefData().getPosition().rot[2] - mDeferredRotation.z()); } @@ -555,8 +406,13 @@ namespace MWRender void Camera::calculateDeferredRotation() { + if (mMode == Mode::Static) + { + mDeferredRotation = osg::Vec3f(); + return; + } MWWorld::Ptr ptr = mTrackingPtr; - if (isVanityOrPreviewModeEnabled() || ptr.isEmpty()) + if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty()) return; if (mFirstPersonView) { @@ -566,6 +422,8 @@ namespace MWRender mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); + if (!mDeferredRotationAllowed) + mDeferredRotationDisabled = true; } } diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 1a5477e89c..280d309256 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWRENDER_CAMERA_H #define GAME_MWRENDER_CAMERA_H +#include #include #include @@ -24,73 +25,8 @@ namespace MWRender class Camera { public: - enum class Mode { Normal, Vanity, Preview, StandingPreview }; + enum class Mode : int {Static = 0, FirstPerson = 1, ThirdPerson = 2, Vanity = 3, Preview = 4}; - private: - MWWorld::Ptr mTrackingPtr; - osg::ref_ptr mTrackingNode; - float mHeightScale; - - osg::ref_ptr mCamera; - - NpcAnimation *mAnimation; - - bool mFirstPersonView; - Mode mMode; - bool mVanityAllowed; - bool mStandingPreviewAllowed; - bool mDeferredRotationAllowed; - - float mNearest; - float mFurthest; - bool mIsNearest; - - float mHeight, mBaseCameraDistance; - float mPitch, mYaw, mRoll; - - bool mVanityToggleQueued; - bool mVanityToggleQueuedValue; - bool mViewModeToggleQueued; - - float mCameraDistance; - float mMaxNextCameraDistance; - - osg::Vec3d mFocalPointAdjustment; - osg::Vec2d mFocalPointCurrentOffset; - osg::Vec2d mFocalPointTargetOffset; - float mFocalPointTransitionSpeedCoef; - bool mSkipFocalPointTransition; - - // This fields are used to make focal point transition smooth if previous transition was not finished. - float mPreviousTransitionInfluence; - osg::Vec2d mFocalPointTransitionSpeed; - osg::Vec2d mPreviousTransitionSpeed; - osg::Vec2d mPreviousExtraOffset; - - float mSmoothedSpeed; - float mZoomOutWhenMoveCoef; - bool mDynamicCameraDistanceEnabled; - bool mShowCrosshairInThirdPersonMode; - - bool mHeadBobbingEnabled; - float mHeadBobbingOffset; - float mHeadBobbingWeight; // Value from 0 to 1 for smooth enabling/disabling. - float mTotalMovement; // Needed for head bobbing. - void updateHeadBobbing(float duration); - - void updateFocalPointOffset(float duration); - void updatePosition(); - float getCameraDistanceCorrection() const; - - osg::ref_ptr mUpdateCallback; - - // Used to rotate player to the direction of view after exiting preview or vanity mode. - osg::Vec3f mDeferredRotation; - bool mDeferredRotationDisabled; - void calculateDeferredRotation(); - void updateStandingPreviewMode(); - - public: Camera(osg::Camera* camera); ~Camera(); @@ -99,36 +35,36 @@ namespace MWRender MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; } void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } - void setFocalPointTargetOffset(osg::Vec2d v); + float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; } + void setFocalPointTargetOffset(const osg::Vec2d& v); + osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; } void instantTransition(); - void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; } - void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; } + void showCrosshair(bool v) { mShowCrosshair = v; } /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); /// Reset to defaults - void reset(); + void reset() { setMode(Mode::FirstPerson); } - /// Set where the camera is looking at. Uses Morrowind (euler) angles - /// \param rot Rotation angles in radians - void rotateCamera(float pitch, float yaw, bool adjust); void rotateCameraToTrackingPtr(); + float getPitch() const { return mPitch; } float getYaw() const { return mYaw; } - void setYaw(float angle); + float getRoll() const { return mRoll; } - float getPitch() const { return mPitch; } - void setPitch(float angle); + void setPitch(float angle, bool force = false); + void setYaw(float angle, bool force = false); + void setRoll(float angle) { mRoll = angle; } + + float getExtraPitch() const { return mExtraPitch; } + float getExtraYaw() const { return mExtraYaw; } + void setExtraPitch(float angle) { mExtraPitch = angle; } + void setExtraYaw(float angle) { mExtraYaw = angle; } /// @param Force view mode switch, even if currently not allowed by the animation. void toggleViewMode(bool force=false); - bool toggleVanityMode(bool enable); - void allowVanityMode(bool allow); - - /// @note this may be ignored if an important animation is currently playing - void togglePreviewMode(bool enable); void applyDeferredPreviewRotationToPlayer(float dt); void disableDeferredPreviewRotation() { mDeferredRotationDisabled = true; } @@ -136,29 +72,84 @@ namespace MWRender /// \brief Lowers the camera for sneak. void setSneakOffset(float offset); - bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; } - void processViewChange(); void update(float duration, bool paused=false); - /// Adds distDelta to the camera distance. Switches 3rd/1st person view if distance is less than limit. - void adjustCameraDistance(float distDelta); - - float getCameraDistance() const; + float getCameraDistance() const { return mCameraDistance; } + void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; } void setAnimation(NpcAnimation *anim); - osg::Vec3d getFocalPoint() const; - osg::Vec3d getFocalPointOffset() const; - - /// Stores focal and camera world positions in passed arguments - void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; + osg::Vec3d getTrackedPosition() const { return mTrackedPosition; } + const osg::Vec3d& getPosition() const { return mPosition; } + void setStaticPosition(const osg::Vec3d& pos); - bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; } + bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::Vanity || mMode == Mode::Preview; } Mode getMode() const { return mMode; } + std::optional getQueuedMode() const { return mQueuedMode; } + void setMode(Mode mode, bool force = true); + + void allowCharacterDeferredRotation(bool v) { mDeferredRotationAllowed = v; } + void calculateDeferredRotation(); + void setFirstPersonOffset(const osg::Vec3f& v) { mFirstPersonOffset = v; } + osg::Vec3f getFirstPersonOffset() const { return mFirstPersonOffset; } + + private: + MWWorld::Ptr mTrackingPtr; + osg::ref_ptr mTrackingNode; + osg::Vec3d mTrackedPosition; + float mHeightScale; + + osg::ref_ptr mCamera; + + NpcAnimation *mAnimation; + + // Always 'true' if mMode == `FirstPerson`. Also it is 'true' in `Vanity` or `Preview` modes if + // the camera should return to `FirstPerson` view after it. + bool mFirstPersonView; + + Mode mMode; + std::optional mQueuedMode; + bool mVanityAllowed; + bool mDeferredRotationAllowed; + + bool mProcessViewChange; + + float mHeight; + float mPitch, mYaw, mRoll; + float mExtraPitch = 0, mExtraYaw = 0; + bool mLockPitch = false, mLockYaw = false; + osg::Vec3d mPosition; - bool isNearest() const { return mIsNearest; } + float mCameraDistance, mPreferredCameraDistance; + + osg::Vec3f mFirstPersonOffset{0, 0, 0}; + + osg::Vec2d mFocalPointCurrentOffset; + osg::Vec2d mFocalPointTargetOffset; + float mFocalPointTransitionSpeedCoef; + bool mSkipFocalPointTransition; + + // This fields are used to make focal point transition smooth if previous transition was not finished. + float mPreviousTransitionInfluence; + osg::Vec2d mFocalPointTransitionSpeed; + osg::Vec2d mPreviousTransitionSpeed; + osg::Vec2d mPreviousExtraOffset; + + bool mShowCrosshair; + + osg::Vec3d calculateTrackedPosition() const; + osg::Vec3d calculateFirstPersonPosition(const osg::Vec3d& trackedPosition) const; + osg::Vec3d getFocalPointOffset() const; + void updateFocalPointOffset(float duration); + void updatePosition(); + + osg::ref_ptr mUpdateCallback; + + // Used to rotate player to the direction of view after exiting preview or vanity mode. + osg::Vec3f mDeferredRotation; + bool mDeferredRotationDisabled; }; } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index a9733cddf8..7cca787580 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" @@ -132,43 +133,6 @@ namespace MWRender newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON); } - if (SceneUtil::getReverseZ() && stateset->getAttribute(osg::StateAttribute::DEPTH)) - { - bool depthModified = false; - osg::Depth* depth = static_cast(stateset->getAttribute(osg::StateAttribute::DEPTH)); - depth->getUserValue("depthModified", depthModified); - - if (!depthModified) - { - if (!newStateSet) - { - newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); - node.setStateSet(newStateSet); - } - // Setup standard depth ranges - osg::ref_ptr newDepth = new osg::Depth(*depth); - - switch (newDepth->getFunction()) - { - case osg::Depth::LESS: - newDepth->setFunction(osg::Depth::GREATER); - break; - case osg::Depth::LEQUAL: - newDepth->setFunction(osg::Depth::GEQUAL); - break; - case osg::Depth::GREATER: - newDepth->setFunction(osg::Depth::LESS); - break; - case osg::Depth::GEQUAL: - newDepth->setFunction(osg::Depth::LEQUAL); - break; - default: - break; - } - newStateSet->setAttribute(newDepth, osg::StateAttribute::ON); - newDepth->setUserValue("depthModified", true); - } - } } traverse(node); } @@ -190,7 +154,9 @@ namespace MWRender mTexture->setInternalFormat(GL_RGBA); mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mTexture->setUserValue("premultiplied alpha", true); + + mTextureStateSet = new osg::StateSet; + mTextureStateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); mCamera = new osg::Camera; // hints that the camera is not relative to the master camera @@ -198,8 +164,6 @@ namespace MWRender mCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); mCamera->setClearColor(osg::Vec4(0.f, 0.f, 0.f, 0.f)); mCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - const float fovYDegrees = 12.3f; - mCamera->setProjectionMatrixAsPerspective(fovYDegrees, sizeX/static_cast(sizeY), 0.1f, 10000.f); // zNear and zFar are autocomputed mCamera->setViewport(0, 0, sizeX, sizeY); mCamera->setRenderOrder(osg::Camera::PRE_RENDER); mCamera->attach(osg::Camera::COLOR_BUFFER, mTexture, 0, 0, false, Settings::Manager::getInt("antialiasing", "Video")); @@ -209,6 +173,8 @@ namespace MWRender mCamera->setNodeMask(Mask_RenderToTexture); + SceneUtil::setCameraClearDepth(mCamera); + bool ffp = mResourceSystem->getSceneManager()->getLightingMethod() == SceneUtil::LightingMethod::FFP; osg::ref_ptr lightManager = new SceneUtil::LightManager(ffp); @@ -224,9 +190,14 @@ namespace MWRender defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); stateset->setAttribute(defaultMat); - stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast(mCamera->getProjectionMatrix()))); - stateset->setAttributeAndModes(new osg::Depth, osg::StateAttribute::ON); + const float fovYDegrees = 12.3f; + const float aspectRatio = static_cast(sizeX) / static_cast(sizeY); + const float znear = 0.1f; + const float zfar = 10000.f; + mCamera->setProjectionMatrixAsPerspective(fovYDegrees, aspectRatio, znear, zfar); + osg::Matrixf projectionMatrix = SceneUtil::AutoDepth::isReversed() ? static_cast(SceneUtil::getReversedZProjectionMatrixAsPerspective(fovYDegrees, aspectRatio, znear, zfar)) : static_cast(mCamera->getProjectionMatrix()); + stateset->addUniform(new osg::Uniform("projectionMatrix", projectionMatrix)); SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); @@ -395,7 +366,7 @@ namespace MWRender if(iter != inv.end()) { groupname = "inventoryweapononehand"; - if(iter->getTypeName() == typeid(ESM::Weapon).name()) + if(iter->getType() == ESM::Weapon::sRecordId) { MWWorld::LiveCellRef *ref = iter->get(); int type = ref->mBase->mData.mType; @@ -428,7 +399,7 @@ namespace MWRender mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() && showCarriedLeft) + if(torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { if(!mAnimation->getInfo("torch")) mAnimation->play("torch", 2, Animation::BlendMask_LeftArm, false, diff --git a/apps/openmw/mwrender/characterpreview.hpp b/apps/openmw/mwrender/characterpreview.hpp index 3eb9688465..808ff0801d 100644 --- a/apps/openmw/mwrender/characterpreview.hpp +++ b/apps/openmw/mwrender/characterpreview.hpp @@ -18,6 +18,7 @@ namespace osg class Camera; class Group; class Viewport; + class StateSet; } namespace MWRender @@ -41,6 +42,8 @@ namespace MWRender void rebuild(); osg::ref_ptr getTexture(); + /// Get the osg::StateSet required to render the texture correctly, if any. + osg::StateSet* getTextureStateSet() { return mTextureStateSet; } private: CharacterPreview(const CharacterPreview&); @@ -54,6 +57,7 @@ namespace MWRender osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mTexture; + osg::ref_ptr mTextureStateSet; osg::ref_ptr mCamera; osg::ref_ptr mDrawOnceCallback; diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index f1d28b0634..50dfb68008 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -119,14 +119,14 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) std::string itemModel = item.getClass().getModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { - if(item.getTypeName() == typeid(ESM::Weapon).name()) + if(item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; bonename = MWMechanics::getWeaponType(type)->mAttachBone; if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); + NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) bonename = "Weapon Bone"; } @@ -137,34 +137,15 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) else { bonename = "Shield Bone"; - if (item.getTypeName() == typeid(ESM::Armor).name()) + if (item.getType() == ESM::Armor::sRecordId) { - // Shield body part model should be used if possible. - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - for (const auto& part : item.get()->mBase->mParts.mParts) - { - // Assume all creatures use the male mesh. - if (part.mPart != ESM::PRT_Shield || part.mMale.empty()) - continue; - const ESM::BodyPart *bodypart = store.get().search(part.mMale); - if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty()) - { - itemModel = "meshes\\" + bodypart->mModel; - break; - } - } + itemModel = getShieldMesh(item, false); } } try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(itemModel); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - osg::ref_ptr attached = SceneUtil::attach(node, mObjectRoot, bonename, found->second.get()); + osg::ref_ptr attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); scene.reset(new PartHolder(attached)); @@ -174,7 +155,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) // Crossbows start out with a bolt attached // FIXME: code duplicated from NpcAnimation if (slot == MWWorld::InventoryStore::Slot_CarriedRight && - item.getTypeName() == typeid(ESM::Weapon).name() && + item.getType() == ESM::Weapon::sRecordId && item.get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow); @@ -246,7 +227,7 @@ osg::Group *CreatureWeaponAnimation::getArrowBone() const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return nullptr; int type = weapon->get()->mBase->mData.mType; diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index b248efad4e..ba8749d81c 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -15,8 +14,8 @@ #include #include -#include #include +#include #include @@ -326,8 +325,8 @@ namespace MWRender if (texture) { osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); - auto depth = SceneUtil::createDepth(); - depth->setWriteMask(0); + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index 77c4f0fab5..a39e31bb4d 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -1,33 +1,24 @@ #include "groundcover.hpp" +#include #include #include #include #include +#include #include #include +#include +#include #include -#include "apps/openmw/mwworld/esmstore.hpp" -#include "apps/openmw/mwbase/environment.hpp" -#include "apps/openmw/mwbase/world.hpp" +#include "../mwworld/groundcoverstore.hpp" #include "vismask.hpp" namespace MWRender { - std::string getGroundcoverModel(int type, const std::string& id, const MWWorld::ESMStore& store) - { - switch (type) - { - case ESM::REC_STAT: - return store.get().searchStatic(id)->mModel; - default: - return std::string(); - } - } - class InstancingVisitor : public osg::NodeVisitor { public: @@ -106,6 +97,20 @@ namespace MWRender float mDensity = 0.f; }; + class ViewDistanceCallback : public SceneUtil::NodeCallback + { + public: + ViewDistanceCallback(float dist, const osg::BoundingBox& box) : mViewDistance(dist), mBox(box) {} + void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (Terrain::distance(mBox, nv->getEyePoint()) <= mViewDistance) + traverse(node, nv); + } + private: + float mViewDistance; + osg::BoundingBox mBox; + }; + inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound) { osg::Vec2f size = maxBound - minBound; @@ -122,8 +127,9 @@ namespace MWRender osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { + if (lod > getMaxLodLevel()) + return nullptr; GroundcoverChunkId id = std::make_tuple(center, size); - osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) return static_cast(obj.get()); @@ -137,12 +143,14 @@ namespace MWRender } } - Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density) + Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store) : GenericResourceManager(nullptr) , mSceneManager(sceneManager) , mDensity(density) , mStateset(new osg::StateSet) + , mGroundcoverStore(store) { + setViewDistance(viewDistance); // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties // Force a unified alpha handling instead of data from meshes osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); @@ -152,14 +160,19 @@ namespace MWRender mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1)); mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1)); - mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? static_cast(mSceneManager->getShaderManager().getProgramTemplate()->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; + mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? Shader::ShaderManager::cloneProgram(mSceneManager->getShaderManager().getProgramTemplate()) : osg::ref_ptr(new osg::Program); mProgramTemplate->addBindAttribLocation("aOffset", 6); mProgramTemplate->addBindAttribLocation("aRotation", 7); } + Groundcover::~Groundcover() + { + } + void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + if (mDensity <=0.f) return; + osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f)); osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f)); DensityCalculator calculator(mDensity); @@ -169,35 +182,37 @@ namespace MWRender { for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) { - const ESM::Cell* cell = store.get().searchStatic(cellX, cellY); - if (!cell) continue; + ESM::Cell cell; + mGroundcoverStore.initCell(cell, cellX, cellY); + if (cell.mContextList.empty()) continue; calculator.reset(); - for (size_t i=0; imContextList.size(); ++i) + std::map refs; + for (size_t i=0; imContextList[i].index; + unsigned int index = cell.mContextList[i].index; if (esm.size() <= index) esm.resize(index+1); - cell->restore(esm[index], i); + cell.restore(esm[index], i); ESM::CellRef ref; ref.mRefNum.unset(); bool deleted = false; - while(cell->getNextRef(esm[index], ref, deleted)) + while(cell.getNextRef(esm[index], ref, deleted)) { - if (deleted) continue; - if (!ref.mRefNum.fromGroundcoverFile()) continue; - - if (!calculator.isInstanceEnabled()) continue; - if (!isInChunkBorders(ref, minBound, maxBound)) continue; + if (!deleted && refs.find(ref.mRefNum) == refs.end() && !calculator.isInstanceEnabled()) deleted = true; + if (!deleted && !isInChunkBorders(ref, minBound, maxBound)) deleted = true; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); - int type = store.findStatic(ref.mRefID); - std::string model = getGroundcoverModel(type, ref.mRefID, store); - if (model.empty()) continue; - model = "meshes/" + model; + if (deleted) { refs.erase(ref.mRefNum); continue; } + refs[ref.mRefNum] = std::move(ref); + } + } + for (auto& pair : refs) + { + ESM::CellRef& ref = pair.second; + const std::string& model = mGroundcoverStore.getGroundcoverModel(ref.mRefID); + if (!model.empty()) instances[model].emplace_back(std::move(ref)); - } } } } @@ -220,10 +235,15 @@ namespace MWRender group->addChild(node); } + osg::ComputeBoundsVisitor cbv; + group->accept(cbv); + osg::BoundingBox box = cbv.getBoundingBox(); + group->addCullCallback(new ViewDistanceCallback(getViewDistance(), box)); + group->setStateSet(mStateset); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) - group->setCullCallback(new SceneUtil::LightListCallback); + group->addCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate); mSceneManager->shareState(group); group->getBound(); diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp index ed88f7fe24..26ed8530aa 100644 --- a/apps/openmw/mwrender/groundcover.hpp +++ b/apps/openmw/mwrender/groundcover.hpp @@ -4,7 +4,16 @@ #include #include #include -#include + +namespace MWWorld +{ + class ESMStore; + class GroundcoverStore; +} +namespace osg +{ + class Program; +} namespace MWRender { @@ -12,8 +21,8 @@ namespace MWRender class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: - Groundcover(Resource::SceneManager* sceneManager, float density); - ~Groundcover() = default; + Groundcover(Resource::SceneManager* sceneManager, float density, float viewDistance, const MWWorld::GroundcoverStore& store); + ~Groundcover(); osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override; @@ -35,6 +44,7 @@ namespace MWRender float mDensity; osg::ref_ptr mStateset; osg::ref_ptr mProgramTemplate; + const MWWorld::GroundcoverStore& mGroundcoverStore; typedef std::map> InstanceMap; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); diff --git a/apps/openmw/mwrender/localmap.cpp b/apps/openmw/mwrender/localmap.cpp index 0560d1485a..d9982d35c3 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include #include #include @@ -178,7 +178,7 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f { osg::ref_ptr camera (new osg::Camera); - if (SceneUtil::getReverseZ()) + if (SceneUtil::AutoDepth::isReversed()) camera->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10)); else camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10); @@ -195,17 +195,13 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f camera->setNodeMask(Mask_RenderToTexture); // Disable small feature culling, it's not going to be reliable for this camera - osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::CullStack::SMALL_FEATURE_CULLING); + osg::Camera::CullingMode cullingMode = (osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING) & ~(osg::Camera::SMALL_FEATURE_CULLING); camera->setCullingMode(cullingMode); - osg::ref_ptr stateset = new osg::StateSet; - stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); - - if (SceneUtil::getReverseZ()) - stateset->setAttributeAndModes(SceneUtil::createDepth(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - SceneUtil::setCameraClearDepth(camera); + osg::ref_ptr stateset = new osg::StateSet; + stateset->setAttribute(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::FILL), osg::StateAttribute::OVERRIDE); stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast(camera->getProjectionMatrix())), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); // assign large value to effectively turn off fog @@ -562,8 +558,8 @@ bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) if (!segment.mFogOfWarImage) return false; - nX = std::max(0.f, std::min(1.f, nX)); - nY = std::max(0.f, std::min(1.f, nY)); + nX = std::clamp(nX, 0.f, 1.f); + nY = std::clamp(nY, 0.f, 1.f); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); @@ -647,8 +643,7 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient uint32_t clr = *(uint32_t*)data; uint8_t alpha = (clr >> 24); - - alpha = std::min( alpha, (uint8_t) (std::max(0.f, std::min(1.f, (sqrDist/sqrExploreRadius)))*255) ); + alpha = std::min(alpha, (uint8_t)(std::clamp(sqrDist/sqrExploreRadius, 0.f, 1.f) * 255)); uint32_t val = (uint32_t) (alpha << 24); if ( *data != val) { diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 523f7531af..a3c26aeb59 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -4,19 +4,25 @@ #include #include #include +#include +#include #include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" +#include + namespace MWRender { NavMesh::NavMesh(const osg::ref_ptr& root, bool enabled) : mRootNode(root) + , mGroupStateSet(SceneUtil::makeNavMeshTileStateSet()) + , mDebugDrawStateSet(SceneUtil::DebugDraw::makeStateSet()) , mEnabled(enabled) - , mGeneration(0) - , mRevision(0) + , mId(std::numeric_limits::max()) { } @@ -36,46 +42,71 @@ namespace MWRender return mEnabled; } - void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id, - const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings) + void NavMesh::update(const DetourNavigator::NavMeshCacheItem& navMesh, std::size_t id, + const DetourNavigator::Settings& settings) { - if (!mEnabled || (mGroup && mId == id && mGeneration == generation && mRevision == revision)) + using DetourNavigator::TilePosition; + using DetourNavigator::Version; + + if (!mEnabled || (!mTiles.empty() && mId == id && mVersion == navMesh.getVersion())) return; - mId = id; - mGeneration = generation; - mRevision = revision; - if (mGroup) - mRootNode->removeChild(mGroup); - mGroup = SceneUtil::createNavMeshGroup(navMesh, settings); - if (mGroup) + if (mId != id) { - MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mGroup, "debug"); - mGroup->setNodeMask(Mask_Debug); - mRootNode->addChild(mGroup); + reset(); + mId = id; + } + + mVersion = navMesh.getVersion(); + + std::vector updated; + navMesh.forEachUsedTile([&] (const TilePosition& position, const Version& version, const dtMeshTile& meshTile) + { + updated.push_back(position); + Tile& tile = mTiles[position]; + if (tile.mGroup != nullptr && tile.mVersion == version) + return; + if (tile.mGroup != nullptr) + mRootNode->removeChild(tile.mGroup); + tile.mGroup = SceneUtil::createNavMeshTileGroup(navMesh.getImpl(), meshTile, settings, + mGroupStateSet, mDebugDrawStateSet); + if (tile.mGroup == nullptr) + return; + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(tile.mGroup, "debug"); + tile.mGroup->setNodeMask(Mask_Debug); + mRootNode->addChild(tile.mGroup); + }); + std::sort(updated.begin(), updated.end()); + for (auto it = mTiles.begin(); it != mTiles.end();) + { + if (!std::binary_search(updated.begin(), updated.end(), it->first)) + { + mRootNode->removeChild(it->second.mGroup); + it = mTiles.erase(it); + } + else + ++it; } } void NavMesh::reset() { - if (mGroup) - { - mRootNode->removeChild(mGroup); - mGroup = nullptr; - } + for (auto& [position, tile] : mTiles) + mRootNode->removeChild(tile.mGroup); + mTiles.clear(); } void NavMesh::enable() { - if (mGroup) - mRootNode->addChild(mGroup); + for (const auto& [position, tile] : mTiles) + mRootNode->addChild(tile.mGroup); mEnabled = true; } void NavMesh::disable() { - if (mGroup) - mRootNode->removeChild(mGroup); + for (const auto& [position, tile] : mTiles) + mRootNode->removeChild(tile.mGroup); mEnabled = false; } } diff --git a/apps/openmw/mwrender/navmesh.hpp b/apps/openmw/mwrender/navmesh.hpp index d329b895d7..fd69a3e487 100644 --- a/apps/openmw/mwrender/navmesh.hpp +++ b/apps/openmw/mwrender/navmesh.hpp @@ -1,14 +1,27 @@ #ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H -#include +#include +#include #include +#include +#include + +class dtNavMesh; + namespace osg { class Group; class Geometry; + class StateSet; +} + +namespace DetourNavigator +{ + class NavMeshCacheItem; + struct Settings; } namespace MWRender @@ -21,8 +34,8 @@ namespace MWRender bool toggle(); - void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation, - const std::size_t revision, const DetourNavigator::Settings& settings); + void update(const DetourNavigator::NavMeshCacheItem& navMesh, std::size_t id, + const DetourNavigator::Settings& settings); void reset(); @@ -36,12 +49,19 @@ namespace MWRender } private: + struct Tile + { + DetourNavigator::Version mVersion; + osg::ref_ptr mGroup; + }; + osg::ref_ptr mRootNode; + osg::ref_ptr mGroupStateSet; + osg::ref_ptr mDebugDrawStateSet; bool mEnabled; - std::size_t mId = std::numeric_limits::max(); - std::size_t mGeneration; - std::size_t mRevision; - osg::ref_ptr mGroup; + std::size_t mId; + DetourNavigator::Version mVersion; + std::map mTiles; }; } diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 7c6d82d83d..da361aec25 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -44,6 +45,7 @@ #include "renderbin.hpp" #include "vismask.hpp" #include "util.hpp" +#include "postprocessor.hpp" namespace { @@ -81,34 +83,6 @@ std::string getVampireHead(const std::string& race, bool female) return "meshes\\" + bodyPart->mModel; } -std::string getShieldBodypartMesh(const std::vector& bodyparts, bool female) -{ - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const MWWorld::Store &partStore = store.get(); - for (const auto& part : bodyparts) - { - if (part.mPart != ESM::PRT_Shield) - continue; - - std::string bodypartName; - if (female && !part.mFemale.empty()) - bodypartName = part.mFemale; - else if (!part.mMale.empty()) - bodypartName = part.mMale; - - if (!bodypartName.empty()) - { - const ESM::BodyPart *bodypart = partStore.search(bodypartName); - if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor) - return std::string(); - if (!bodypart->mModel.empty()) - return "meshes\\" + bodypart->mModel; - } - } - - return std::string(); -} - } @@ -330,25 +304,58 @@ void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) } /// @brief A RenderBin callback to clear the depth buffer before rendering. +/// Switches depth attachments to a proxy renderbuffer, reattaches original depth then redraws first person root. +/// This gives a complete depth buffer which can be used for postprocessing, buffer resolves as if depth was never cleared. class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { public: DepthClearCallback() { - mDepth = SceneUtil::createDepth(); + mDepth = new SceneUtil::AutoDepth; mDepth->setWriteMask(true); + + mStateSet = new osg::StateSet; + mStateSet->setAttributeAndModes(new osg::ColorMask(false, false, false, false), osg::StateAttribute::ON); + mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); } void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override { - renderInfo.getState()->applyAttribute(mDepth); + osg::State* state = renderInfo.getState(); + + PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); - glClear(GL_DEPTH_BUFFER_BIT); + state->applyAttribute(mDepth); - bin->drawImplementation(renderInfo, previous); + if (postProcessor && postProcessor->getFirstPersonRBProxy()) + { + osg::GLExtensions* ext = state->get(); + + osg::FrameBufferAttachment(postProcessor->getFirstPersonRBProxy()).attach(*state, GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, ext); + + glClear(GL_DEPTH_BUFFER_BIT); + // color accumulation pass + bin->drawImplementation(renderInfo, previous); + + auto primaryFBO = postProcessor->getMsaaFbo() ? postProcessor->getMsaaFbo() : postProcessor->getFbo(); + primaryFBO->getAttachment(osg::FrameBufferObject::BufferComponent::DEPTH_BUFFER).attach(*state, GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, ext); + + state->pushStateSet(mStateSet); + state->apply(); + // depth accumulation pass + bin->drawImplementation(renderInfo, previous); + state->popStateSet(); + } + else + { + // fallback to standard depth clear when we are not rendering our main scene via an intermediate FBO + glClear(GL_DEPTH_BUFFER_BIT); + bin->drawImplementation(renderInfo, previous); + } } osg::ref_ptr mDepth; + osg::ref_ptr mStateSet; }; /// Overrides Field of View to given value for rendering the subgraph. @@ -513,14 +520,9 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } -std::string NpcAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const +std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { - std::string mesh = shield.getClass().getModel(shield); - const ESM::Armor *armor = shield.get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - // Try to recover the body part model, use ground model as a fallback otherwise. - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + std::string mesh = getShieldMesh(shield, !mNpc->isMale()); if (mesh.empty()) return std::string(); @@ -594,13 +596,13 @@ void NpcAnimation::updateParts() int prio = 1; bool enchantedGlow = !store->getClass().getEnchantment(*store).empty(); osg::Vec4f glowColor = store->getClass().getEnchantmentColor(*store); - if(store->getTypeName() == typeid(ESM::Clothing).name()) + if(store->getType() == ESM::Clothing::sRecordId) { prio = ((slotlist[i].mBasePriority+1)<<1) + 0; const ESM::Clothing *clothes = store->get()->mBase; addPartGroup(slotlist[i].mSlot, prio, clothes->mParts.mParts, enchantedGlow, &glowColor); } - else if(store->getTypeName() == typeid(ESM::Armor).name()) + else if(store->getType() == ESM::Armor::sRecordId) { prio = ((slotlist[i].mBasePriority+1)<<1) + 1; const ESM::Armor *armor = store->get()->mBase; @@ -640,11 +642,11 @@ void NpcAnimation::updateParts() { MWWorld::ConstContainerStoreIterator store = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); MWWorld::ConstPtr part; - if(store != inv.end() && (part=*store).getTypeName() == typeid(ESM::Light).name()) + if(store != inv.end() && (part=*store).getType() == ESM::Light::sRecordId) { const ESM::Light *light = part.get()->mBase; addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, - 1, "meshes\\"+light->mModel); + 1, "meshes\\"+light->mModel, false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), light); } @@ -674,16 +676,9 @@ void NpcAnimation::updateParts() -PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) +PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { - osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); - - const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); - if (found == nodeMap.end()) - throw std::runtime_error("Can't find attachment node " + bonename); - - osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second); + osg::ref_ptr attached = attach(model, bonename, bonefilter, isLight); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); @@ -756,7 +751,7 @@ bool NpcAnimation::isFemalePart(const ESM::BodyPart* bodypart) return bodypart->mData.mFlags & ESM::BodyPart::BPF_Female; } -bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor) +bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if(priority <= mPartPriorities[type]) return false; @@ -771,7 +766,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()) + if(weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId) { int weaponType = weapon->get()->mBase->mData.mType; const std::string weaponBonename = MWMechanics::getWeaponType(weaponType)->mAttachBone; @@ -779,7 +774,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g if (weaponBonename != bonename) { const NodeMap& nodeMap = getNodeMap(); - NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(weaponBonename)); + NodeMap::const_iterator found = nodeMap.find(weaponBonename); if (found != nodeMap.end()) bonename = weaponBonename; } @@ -788,7 +783,7 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g // PRT_Hair seems to be the only type that breaks consistency and uses a filter that's different from the attachment bone const std::string bonefilter = (type == ESM::PRT_Hair) ? "hair" : bonename; - mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor); + mObjectParts[type] = insertBoundedPart(mesh, bonename, bonefilter, enchantedGlow, glowColor, isLight); } catch (std::exception& e) { @@ -843,14 +838,18 @@ bool NpcAnimation::addOrReplaceIndividualPart(ESM::PartReferenceType type, int g } } } + SceneUtil::ForceControllerSourcesVisitor assignVisitor(src); + node->accept(assignVisitor); } - else if (type == ESM::PRT_Weapon) - src = mWeaponAnimationTime; else - src.reset(new NullAnimationTime); - - SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); - node->accept(assignVisitor); + { + if (type == ESM::PRT_Weapon) + src = mWeaponAnimationTime; + else + src.reset(new NullAnimationTime); + SceneUtil::AssignControllerSourcesVisitor assignVisitor(src); + node->accept(assignVisitor); + } } return true; @@ -943,7 +942,7 @@ void NpcAnimation::showWeapons(bool showWeapon) mesh, !weapon->getClass().getEnchantment(*weapon).empty(), &glowColor); // Crossbows start out with a bolt attached - if (weapon->getTypeName() == typeid(ESM::Weapon).name() && + if (weapon->getType() == ESM::Weapon::sRecordId && weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanCrossbow) { int ammotype = MWMechanics::getWeaponType(ESM::Weapon::MarksmanCrossbow)->mAmmoType; @@ -975,19 +974,16 @@ void NpcAnimation::showCarriedLeft(bool show) osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); std::string mesh = iter->getClass().getModel(*iter); // For shields we must try to use the body part model - if (iter->getTypeName() == typeid(ESM::Armor).name()) + if (iter->getType() == ESM::Armor::sRecordId) { - const ESM::Armor *armor = iter->get()->mBase; - const std::vector& bodyparts = armor->mParts.mParts; - if (!bodyparts.empty()) - mesh = getShieldBodypartMesh(bodyparts, !mNpc->isMale()); + mesh = getShieldMesh(*iter, !mNpc->isMale()); } if (mesh.empty() || addOrReplaceIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1, - mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor)) + mesh, !iter->getClass().getEnchantment(*iter).empty(), &glowColor, iter->getType() == ESM::Light::sRecordId)) { if (mesh.empty()) reserveIndividualPart(ESM::PRT_Shield, MWWorld::InventoryStore::Slot_CarriedLeft, 1); - if (iter->getTypeName() == typeid(ESM::Light).name() && mObjectParts[ESM::PRT_Shield]) + if (iter->getType() == ESM::Light::sRecordId && mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), iter->get()->mBase); } } @@ -1033,7 +1029,7 @@ osg::Group* NpcAnimation::getArrowBone() const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end() || weapon->getTypeName() != typeid(ESM::Weapon).name()) + if(weapon == inv.end() || weapon->getType() != ESM::Weapon::sRecordId) return nullptr; int type = weapon->get()->mBase->mData.mType; diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index b511a52f37..2dcfac3036 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -83,13 +83,13 @@ private: NpcType getNpcType() const; PartHolderPtr insertBoundedPart(const std::string &model, const std::string &bonename, - const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor=nullptr); + const std::string &bonefilter, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight); void removeIndividualPart(ESM::PartReferenceType type); void reserveIndividualPart(ESM::PartReferenceType type, int group, int priority); bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, const std::string &mesh, - bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); + bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr, bool isLight = false); void removePartGroup(int group); void addPartGroup(int group, int priority, const std::vector &parts, bool enchantedGlow=false, osg::Vec4f* glowColor=nullptr); @@ -105,7 +105,7 @@ private: protected: void addControllers() override; bool isArrowAttached() const override; - std::string getShieldMesh(const MWWorld::ConstPtr& shield) const override; + std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const override; public: /** diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 88c3d4ba02..756769bc7d 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -358,6 +359,7 @@ namespace MWRender stateset->setAttribute(m); stateset->addUniform(new osg::Uniform("colorMode", 0)); stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); + stateset->addUniform(new osg::Uniform("specStrength", 1.f)); node.setStateSet(stateset); } }; @@ -427,11 +429,9 @@ namespace MWRender continue; if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; if (deleted) { refs.erase(ref.mRefNum); continue; } - if (ref.mRefNum.fromGroundcoverFile()) continue; refs[ref.mRefNum] = std::move(ref); } } @@ -443,7 +443,6 @@ namespace MWRender for (auto [ref, deleted] : cell->mLeasedRefs) { if (deleted) { refs.erase(ref.mRefNum); continue; } - Misc::StringUtils::lowerCaseInPlace(ref.mRefID); int type = store.findStatic(ref.mRefID); if (!typeFilter(type,size>=2)) continue; refs[ref.mRefNum] = std::move(ref); @@ -590,15 +589,36 @@ namespace MWRender if (!activeGrid && minSizeMerged != minSize && cnode->getBound().radius2() * cref->mScale*cref->mScale < (viewPoint-pos).length2()*minSizeMerged*minSizeMerged) continue; - osg::Matrixf matrix; - matrix.preMultTranslate(pos - worldCenter); - matrix.preMultRotate( osg::Quat(ref.mPos.rot[2], osg::Vec3f(0,0,-1)) * + osg::Vec3f nodePos = pos - worldCenter; + osg::Quat nodeAttitude = osg::Quat(ref.mPos.rot[2], osg::Vec3f(0,0,-1)) * osg::Quat(ref.mPos.rot[1], osg::Vec3f(0,-1,0)) * - osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1,0,0)) ); - matrix.preMultScale(osg::Vec3f(ref.mScale, ref.mScale, ref.mScale)); - osg::ref_ptr trans = new osg::MatrixTransform(matrix); - trans->setDataVariance(osg::Object::STATIC); + osg::Quat(ref.mPos.rot[0], osg::Vec3f(-1,0,0)); + osg::Vec3f nodeScale = osg::Vec3f(ref.mScale, ref.mScale, ref.mScale); + osg::ref_ptr trans; + if (merge) + { + // Optimizer currently supports only MatrixTransforms. + osg::Matrixf matrix; + matrix.preMultTranslate(nodePos); + matrix.preMultRotate(nodeAttitude); + matrix.preMultScale(nodeScale); + trans = new osg::MatrixTransform(matrix); + trans->setDataVariance(osg::Object::STATIC); + } + else + { + trans = new SceneUtil::PositionAttitudeTransform; + SceneUtil::PositionAttitudeTransform* pat = static_cast(trans.get()); + pat->setPosition(nodePos); + pat->setScale(nodeScale); + pat->setAttitude(nodeAttitude); + } + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateMultiRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by needvbo() in optimizer.cpp) copyop.setCopyFlags(merge ? osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES : osg::CopyOp::DEEP_COPY_NODES); copyop.mOptimizeBillboards = (size > 1/4.f); copyop.mNodePath.push_back(trans); @@ -627,7 +647,8 @@ namespace MWRender } if (numinstances > 0) { - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, we hint to the cache that it's still being used and should be kept in cache templateRefs->addRef(cnode); if (pair.second.mNeedCompile) @@ -712,12 +733,8 @@ namespace MWRender } void clampToCell(osg::Vec3f& cellPos) { - osg::Vec2i min (mCell.x(), mCell.y()); - osg::Vec2i max (mCell.x()+1, mCell.y()+1); - if (cellPos.x() < min.x()) cellPos.x() = min.x(); - if (cellPos.x() > max.x()) cellPos.x() = max.x(); - if (cellPos.y() < min.y()) cellPos.y() = min.y(); - if (cellPos.y() > max.y()) cellPos.y() = max.y(); + cellPos.x() = std::clamp(cellPos.x(), mCell.x(), mCell.x() + 1); + cellPos.y() = std::clamp(cellPos.y(), mCell.y(), mCell.y() + 1); } osg::Vec3f mPosition; osg::Vec2i mCell; diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index ec1c4397bf..e208d7191e 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -4,7 +4,6 @@ #include #include -#include #include "../mwworld/ptr.hpp" #include "../mwworld/class.hpp" @@ -18,10 +17,9 @@ namespace MWRender { -Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue) +Objects::Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode) : mRootNode(rootNode) , mResourceSystem(resourceSystem) - , mUnrefQueue(unrefQueue) { } @@ -117,9 +115,6 @@ bool Objects::removeObject (const MWWorld::Ptr& ptr) PtrAnimationMap::iterator iter = mObjects.find(ptr); if(iter != mObjects.end()) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); - mObjects.erase(iter); if (ptr.getClass().isActor()) @@ -146,14 +141,11 @@ void Objects::removeCell(const MWWorld::CellStore* store) MWWorld::Ptr ptr = iter->second->getPtr(); if(ptr.getCell() == store) { - if (mUnrefQueue.get()) - mUnrefQueue->push(iter->second); - - if (ptr.getClass().isNpc() && ptr.getRefData().getCustomData()) + if (ptr.getClass().isActor() && ptr.getRefData().getCustomData()) { - MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); - invStore.setInvListener(nullptr, ptr); - invStore.setContListener(nullptr); + if (ptr.getClass().hasInventoryStore(ptr)) + ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr, ptr); + ptr.getClass().getContainerStore(ptr).setContListener(nullptr); } mObjects.erase(iter++); @@ -166,8 +158,6 @@ void Objects::removeCell(const MWWorld::CellStore* store) if(cell != mCellSceneNodes.end()) { cell->second->getParent(0)->removeChild(cell->second); - if (mUnrefQueue.get()) - mUnrefQueue->push(cell->second); mCellSceneNodes.erase(cell); } } diff --git a/apps/openmw/mwrender/objects.hpp b/apps/openmw/mwrender/objects.hpp index 98ebb95d98..5f8b7dfc9e 100644 --- a/apps/openmw/mwrender/objects.hpp +++ b/apps/openmw/mwrender/objects.hpp @@ -24,11 +24,6 @@ namespace MWWorld class CellStore; } -namespace SceneUtil -{ - class UnrefQueue; -} - namespace MWRender{ class Animation; @@ -66,12 +61,10 @@ class Objects{ Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mUnrefQueue; - void insertBegin(const MWWorld::Ptr& ptr); public: - Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode, SceneUtil::UnrefQueue* unrefQueue); + Objects(Resource::ResourceSystem* resourceSystem, osg::ref_ptr rootNode); ~Objects(); /// @param animated Attempt to load separate keyframes from a .kf file matching the model file? diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index bc00676d7b..53fae4cd7d 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -9,12 +9,11 @@ #include #include -#include +#include #include #include #include "vismask.hpp" -#include "renderingmanager.hpp" namespace { @@ -93,17 +92,53 @@ namespace MWRender::PostProcessor* mPostProcessor; }; + + // Copies the currently bound depth attachment to a new texture so drawables in transparent renderbin can safely sample from depth. + class OpaqueDepthCopyCallback : public osgUtil::RenderBin::DrawCallback + { + public: + OpaqueDepthCopyCallback(osg::ref_ptr opaqueDepthTex, osg::ref_ptr sourceFbo) + : mOpaqueDepthFbo(new osg::FrameBufferObject) + , mSourceFbo(sourceFbo) + , mOpaqueDepthTex(opaqueDepthTex) + { + mOpaqueDepthFbo->setAttachment(osg::FrameBufferObject::BufferComponent::DEPTH_BUFFER, osg::FrameBufferAttachment(opaqueDepthTex)); + } + + void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override + { + if (bin->getStage()->getFrameBufferObject() == mSourceFbo) + { + osg::State& state = *renderInfo.getState(); + osg::GLExtensions* ext = state.get(); + + mSourceFbo->apply(state, osg::FrameBufferObject::READ_FRAMEBUFFER); + mOpaqueDepthFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); + + ext->glBlitFramebuffer(0, 0, mOpaqueDepthTex->getTextureWidth(), mOpaqueDepthTex->getTextureHeight(), 0, 0, mOpaqueDepthTex->getTextureWidth(), mOpaqueDepthTex->getTextureHeight(), GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + mSourceFbo->apply(state); + } + + bin->drawImplementation(renderInfo, previous); + } + private: + osg::ref_ptr mOpaqueDepthFbo; + osg::ref_ptr mSourceFbo; + osg::ref_ptr mOpaqueDepthTex; + }; } namespace MWRender { - PostProcessor::PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode) + PostProcessor::PostProcessor(osgViewer::Viewer* viewer, osg::Group* rootNode) : mViewer(viewer) , mRootNode(new osg::Group) , mDepthFormat(GL_DEPTH_COMPONENT24) - , mRendering(rendering) { - if (!SceneUtil::getReverseZ()) + bool softParticles = Settings::Manager::getBool("soft particles", "Shaders"); + + if (!SceneUtil::AutoDepth::isReversed() && !softParticles) return; osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); @@ -124,17 +159,22 @@ namespace MWRender return; } - if (osg::isGLExtensionSupported(contextID, "GL_ARB_depth_buffer_float")) - mDepthFormat = GL_DEPTH_COMPONENT32F; - else if (osg::isGLExtensionSupported(contextID, "GL_NV_depth_buffer_float")) - mDepthFormat = GL_DEPTH_COMPONENT32F_NV; - else + if (SceneUtil::AutoDepth::isReversed()) { - // TODO: Once we have post-processing implemented we want to skip this return and continue with setup. - // Rendering to a FBO to fullscreen geometry has overhead (especially when MSAA is enabled) and there are no - // benefits if no floating point depth formats are supported. - Log(Debug::Warning) << errPreamble << "'GL_ARB_depth_buffer_float' and 'GL_NV_depth_buffer_float' unsupported."; - return; + if (osg::isGLExtensionSupported(contextID, "GL_ARB_depth_buffer_float")) + mDepthFormat = GL_DEPTH_COMPONENT32F; + else if (osg::isGLExtensionSupported(contextID, "GL_NV_depth_buffer_float")) + mDepthFormat = GL_DEPTH_COMPONENT32F_NV; + else + { + // TODO: Once we have post-processing implemented we want to skip this return and continue with setup. + // Rendering to a FBO to fullscreen geometry has overhead (especially when MSAA is enabled) and there are no + // benefits if no floating point depth formats are supported. + Log(Debug::Warning) << errPreamble << "'GL_ARB_depth_buffer_float' and 'GL_NV_depth_buffer_float' unsupported."; + + if (!softParticles) + return; + } } int width = viewer->getCamera()->getViewport()->width(); @@ -165,6 +205,12 @@ namespace MWRender mDepthTex->dirtyTextureObject(); mSceneTex->dirtyTextureObject(); + if (mOpaqueDepthTex) + { + mOpaqueDepthTex->setTextureSize(width, height); + mOpaqueDepthTex->dirtyTextureObject(); + } + int samples = Settings::Manager::getInt("antialiasing", "Video"); mFbo = new osg::FrameBufferObject; @@ -183,9 +229,14 @@ namespace MWRender mMsaaFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(depthRB)); } + if (const auto depthProxy = std::getenv("OPENMW_ENABLE_DEPTH_CLEAR_PROXY")) + mFirstPersonDepthRBProxy = new osg::RenderBuffer(width, height, mDepthTex->getInternalFormat(), samples); + + if (Settings::Manager::getBool("soft particles", "Shaders")) + osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(new OpaqueDepthCopyCallback(mOpaqueDepthTex, mMsaaFbo ? mMsaaFbo : mFbo)); + mViewer->getCamera()->resize(width, height); mHUDCamera->resize(width, height); - mRendering.updateProjectionMatrix(); } void PostProcessor::createTexturesAndCamera(int width, int height) @@ -201,6 +252,12 @@ namespace MWRender mDepthTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mDepthTex->setResizeNonPowerOfTwoHint(false); + if (Settings::Manager::getBool("soft particles", "Shaders")) + { + mOpaqueDepthTex = new osg::Texture2D(*mDepthTex); + mOpaqueDepthTex->setName("opaqueTexMap"); + } + mSceneTex = new osg::Texture2D; mSceneTex->setTextureSize(width, height); mSceneTex->setSourceFormat(GL_RGB); diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index f93f3a5b66..f2ef238737 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -14,17 +14,17 @@ namespace osgViewer namespace MWRender { - class RenderingManager; - class PostProcessor : public osg::Referenced { public: - PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode); + PostProcessor(osgViewer::Viewer* viewer, osg::Group* rootNode); auto getMsaaFbo() { return mMsaaFbo; } auto getFbo() { return mFbo; } + auto getFirstPersonRBProxy() { return mFirstPersonDepthRBProxy; } int getDepthFormat() { return mDepthFormat; } + osg::ref_ptr getOpaqueDepthTex() { return mOpaqueDepthTex; } void resize(int width, int height); @@ -37,13 +37,13 @@ namespace MWRender osg::ref_ptr mMsaaFbo; osg::ref_ptr mFbo; + osg::ref_ptr mFirstPersonDepthRBProxy; osg::ref_ptr mSceneTex; osg::ref_ptr mDepthTex; + osg::ref_ptr mOpaqueDepthTex; int mDepthFormat; - - RenderingManager& mRendering; }; } diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp index 5afa78cd93..f108536242 100644 --- a/apps/openmw/mwrender/recastmesh.cpp +++ b/apps/openmw/mwrender/recastmesh.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include @@ -60,7 +62,6 @@ namespace MWRender it->second.mValue = group; it->second.mGeneration = tile->second->getGeneration(); it->second.mRevision = tile->second->getRevision(); - continue; } ++it; diff --git a/apps/openmw/mwrender/recastmesh.hpp b/apps/openmw/mwrender/recastmesh.hpp index 729438dbe5..194ec04a62 100644 --- a/apps/openmw/mwrender/recastmesh.hpp +++ b/apps/openmw/mwrender/recastmesh.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_MWRENDER_RECASTMESH_H #define OPENMW_MWRENDER_RECASTMESH_H -#include +#include #include @@ -13,6 +13,11 @@ namespace osg class Geometry; } +namespace DetourNavigator +{ + struct Settings; +} + namespace MWRender { class RecastMesh diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 331ddeca7b..73ee40435e 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -32,12 +32,11 @@ #include -#include +#include #include #include #include #include -#include #include #include @@ -50,6 +49,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/groundcoverstore.hpp" #include "../mwgui/loadingscreen.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" @@ -60,7 +60,6 @@ #include "vismask.hpp" #include "pathgrid.hpp" #include "camera.hpp" -#include "viewovershoulder.hpp" #include "water.hpp" #include "terrainstorage.hpp" #include "navmesh.hpp" @@ -92,6 +91,7 @@ namespace MWRender stateset->addUniform(new osg::Uniform("linearFac", 0.f)); stateset->addUniform(new osg::Uniform("near", 0.f)); stateset->addUniform(new osg::Uniform("far", 0.f)); + stateset->addUniform(new osg::Uniform("screenRes", osg::Vec2f{})); if (mUsePlayerUniforms) { stateset->addUniform(new osg::Uniform("windSpeed", 0.0f)); @@ -117,6 +117,10 @@ namespace MWRender if (uFar) uFar->set(mFar); + auto* uScreenRes = stateset->getUniform("screenRes"); + if (uScreenRes) + uScreenRes->set(mScreenRes); + if (mUsePlayerUniforms) { auto* windSpeed = stateset->getUniform("windSpeed"); @@ -149,6 +153,11 @@ namespace MWRender mFar = far; } + void setScreenRes(float width, float height) + { + mScreenRes = osg::Vec2f(width, height); + } + void setWindSpeed(float windSpeed) { mWindSpeed = windSpeed; @@ -168,6 +177,7 @@ namespace MWRender bool mUsePlayerUniforms; float mWindSpeed; osg::Vec3f mPlayerPos; + osg::Vec2f mScreenRes; }; class StateUpdater : public SceneUtil::StateSetUpdater @@ -284,25 +294,24 @@ namespace MWRender RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator) + const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore) : mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) - , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) + // TODO: Near clip should not need to be bounded like this, but too small values break OSG shadow calculations CPU-side. + // See issue: #6072 + , mNearClip(std::max(0.005f, Settings::Manager::getFloat("near clip", "Camera"))) + , mViewDistance(Settings::Manager::getFloat("viewing distance", "Camera")) , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) + , mFieldOfView(std::clamp(Settings::Manager::getFloat("field of view", "Camera"), 1.f, 179.f)) + , mFirstPersonFieldOfView(std::clamp(Settings::Manager::getFloat("first person field of view", "Camera"), 1.f, 179.f)) { - bool reverseZ = SceneUtil::getReverseZ(); - - if (reverseZ) - Log(Debug::Info) << "Using reverse-z depth buffer"; - else - Log(Debug::Info) << "Using standard depth buffer"; - + bool reverseZ = SceneUtil::AutoDepth::isReversed(); auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); @@ -340,14 +349,14 @@ namespace MWRender shadowCastingTraversalMask |= Mask_Actor; if (Settings::Manager::getBool("player shadows", "Shadows")) shadowCastingTraversalMask |= Mask_Player; - if (Settings::Manager::getBool("terrain shadows", "Shadows")) - shadowCastingTraversalMask |= Mask_Terrain; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; if (Settings::Manager::getBool("object shadows", "Shadows")) shadowCastingTraversalMask |= (Mask_Object|Mask_Static); + if (Settings::Manager::getBool("terrain shadows", "Shadows")) + shadowCastingTraversalMask |= Mask_Terrain; - mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, mResourceSystem->getSceneManager()->getShaderManager())); + mShadowManager.reset(new SceneUtil::ShadowManager(sceneRoot, mRootNode, shadowCastingTraversalMask, indoorShadowCastingTraversalMask, Mask_Terrain|Mask_Object|Mask_Static, mResourceSystem->getSceneManager()->getShaderManager())); Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); @@ -382,7 +391,7 @@ namespace MWRender mRecastMesh.reset(new RecastMesh(mRootNode, Settings::Manager::getBool("enable recast mesh render", "Navigator"))); mPathgrid.reset(new Pathgrid(mRootNode)); - mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get())); + mObjects.reset(new Objects(mResourceSystem, sceneRoot)); if (getenv("OPENMW_DONT_PRECOMPILE") == nullptr) { @@ -429,18 +438,15 @@ namespace MWRender mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug)); mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); - mTerrain->setWorkQueue(mWorkQueue.get()); if (groundcover) { float density = Settings::Manager::getFloat("density", "Groundcover"); density = std::clamp(density, 0.f, 1.f); - mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density)); + mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density, groundcoverDistance, groundcoverStore)); static_cast(mTerrain.get())->addChunkManager(mGroundcover.get()); mResourceSystem->addResourceManager(mGroundcover.get()); - - mGroundcover->setViewDistance(groundcoverDistance); } mStateUpdater = new StateUpdater; @@ -449,8 +455,9 @@ namespace MWRender mSharedUniformStateUpdater = new SharedUniformStateUpdater(groundcover); rootNode->addUpdateCallback(mSharedUniformStateUpdater); - mPostProcessor = new PostProcessor(*this, viewer, mRootNode); + mPostProcessor = new PostProcessor(viewer, mRootNode); resourceSystem->getSceneManager()->setDepthFormat(mPostProcessor->getDepthFormat()); + resourceSystem->getSceneManager()->setOpaqueDepthTex(mPostProcessor->getOpaqueDepthTex()); if (reverseZ && !SceneUtil::isFloatingPointDepthFormat(mPostProcessor->getDepthFormat())) Log(Debug::Warning) << "Floating point depth format not in use but reverse-z buffer is enabled, consider disabling it."; @@ -459,8 +466,6 @@ namespace MWRender mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); mCamera.reset(new Camera(mViewer->getCamera())); - if (Settings::Manager::getBool("view over shoulder", "Camera")) - mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get())); mScreenshotManager.reset(new ScreenshotManager(viewer, mRootNode, sceneRoot, mResourceSystem, mWater.get())); @@ -487,6 +492,7 @@ namespace MWRender defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); mFog.reset(new FogManager()); @@ -515,14 +521,6 @@ namespace MWRender NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - // TODO: Near clip should not need to be bounded like this, but too small values break OSG shadow calculations CPU-side. - // See issue: #6072 - mNearClip = std::max(0.005f, Settings::Manager::getFloat("near clip", "Camera")); - mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); - float fov = Settings::Manager::getFloat("field of view", "Camera"); - mFieldOfView = std::min(std::max(1.f, fov), 179.f); - float firstPersonFov = Settings::Manager::getFloat("first person field of view", "Camera"); - mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); @@ -535,7 +533,7 @@ namespace MWRender if (reverseZ) { osg::ref_ptr clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::ZERO_TO_ONE); - mRootNode->getOrCreateStateSet()->setAttributeAndModes(SceneUtil::createDepth(), osg::StateAttribute::ON); + mRootNode->getOrCreateStateSet()->setAttributeAndModes(new SceneUtil::AutoDepth, osg::StateAttribute::ON); mRootNode->getOrCreateStateSet()->setAttributeAndModes(clipcontrol, osg::StateAttribute::ON); } @@ -570,11 +568,6 @@ namespace MWRender return mWorkQueue.get(); } - SceneUtil::UnrefQueue* RenderingManager::getUnrefQueue() - { - return mUnrefQueue.get(); - } - Terrain::World* RenderingManager::getTerrain() { return mTerrain.get(); @@ -762,12 +755,12 @@ namespace MWRender else if (mode == Render_Scene) { unsigned int mask = mViewer->getCamera()->getCullMask(); - bool enabled = mask&Mask_Scene; - enabled = !enabled; + bool enabled = !(mask&sToggleWorldMask); if (enabled) - mask |= Mask_Scene; + mask |= sToggleWorldMask; else - mask &= ~Mask_Scene; + mask &= ~sToggleWorldMask; + mWater->showWorld(enabled); mViewer->getCamera()->setCullMask(mask); return enabled; } @@ -805,8 +798,6 @@ namespace MWRender { reportStats(); - mUnrefQueue->flush(mWorkQueue.get()); - float rainIntensity = mSky->getPrecipitationAlpha(); mWater->setRainIntensity(rainIntensity); @@ -827,15 +818,9 @@ namespace MWRender updateNavMesh(); updateRecastMesh(); - if (mViewOverShoulderController) - mViewOverShoulderController->update(); mCamera->update(dt, paused); - osg::Vec3d focal, cameraPos; - mCamera->getPosition(focal, cameraPos); - mCurrentCameraPos = cameraPos; - - bool isUnderwater = mWater->isUnderwater(cameraPos); + bool isUnderwater = mWater->isUnderwater(mCamera->getPosition()); mStateUpdater->setFogStart(mFog->getFogStart(isUnderwater)); mStateUpdater->setFogEnd(mFog->getFogEnd(isUnderwater)); setFogColor(mFog->getFogColor(isUnderwater)); @@ -913,15 +898,8 @@ namespace MWRender return false; } - unsigned int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); - - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(0); - mScreenshotManager->screenshot360(image); - mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); - return true; } @@ -1167,7 +1145,7 @@ namespace MWRender mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance); - if (SceneUtil::getReverseZ()) + if (SceneUtil::AutoDepth::isReversed()) { mSharedUniformStateUpdater->setLinearFac(-mNearClip / (mViewDistance - mNearClip) - 1.f); mSharedUniformStateUpdater->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance)); @@ -1177,6 +1155,7 @@ namespace MWRender mSharedUniformStateUpdater->setNear(mNearClip); mSharedUniformStateUpdater->setFar(mViewDistance); + mSharedUniformStateUpdater->setScreenRes(mViewer->getCamera()->getViewport()->width(), mViewer->getCamera()->getViewport()->height()); // Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may disappear. // Limit FOV here just for sure, otherwise viewing distance can be too high. @@ -1224,27 +1203,32 @@ namespace MWRender unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (stats->collectStats("resource")) { - stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getNumItems()); - mTerrain->reportStats(frameNumber, stats); } } void RenderingManager::processChangedSettings(const Settings::CategorySettingVector &changed) { + // Only perform a projection matrix update once if a relevant setting is changed. + bool updateProjection = false; + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Camera" && it->second == "field of view") { mFieldOfView = Settings::Manager::getFloat("field of view", "Camera"); - updateProjectionMatrix(); + updateProjection = true; + } + else if (it->first == "Video" && (it->second == "resolution x" || it->second == "resolution y")) + { + updateProjection = true; } else if (it->first == "Camera" && it->second == "viewing distance") { mViewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); if(!Settings::Manager::getBool("use distant fog", "Fog")) mStateUpdater->setFogEnd(mViewDistance); - updateProjectionMatrix(); + updateProjection = true; } else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || @@ -1287,6 +1271,11 @@ namespace MWRender } } } + + if (updateProjection) + { + updateProjectionMatrix(); + } } float RenderingManager::getNearClipDistance() const @@ -1389,9 +1378,7 @@ namespace MWRender { try { - const auto locked = it->second->lockConst(); - mNavMesh->update(locked->getImpl(), mNavMeshNumber, locked->getGeneration(), - locked->getNavMeshRevision(), mNavigator.getSettings()); + mNavMesh->update(*it->second->lockConst(), mNavMeshNumber, mNavigator.getSettings()); } catch (const std::exception& e) { diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index bd8bbe4694..119c85f82f 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -59,7 +59,6 @@ namespace SceneUtil { class ShadowManager; class WorkQueue; - class UnrefQueue; } namespace DetourNavigator @@ -68,6 +67,11 @@ namespace DetourNavigator struct Settings; } +namespace MWWorld +{ + class GroundcoverStore; +} + namespace MWRender { class StateUpdater; @@ -80,7 +84,6 @@ namespace MWRender class NpcAnimation; class Pathgrid; class Camera; - class ViewOverShoulderController; class Water; class TerrainStorage; class LandManager; @@ -96,7 +99,7 @@ namespace MWRender public: RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, - const std::string& resourcePath, DetourNavigator::Navigator& navigator); + const std::string& resourcePath, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore); ~RenderingManager(); osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation(); @@ -106,7 +109,6 @@ namespace MWRender Resource::ResourceSystem* getResourceSystem(); SceneUtil::WorkQueue* getWorkQueue(); - SceneUtil::UnrefQueue* getUnrefQueue(); Terrain::World* getTerrain(); void preloadCommonAssets(); @@ -209,7 +211,6 @@ namespace MWRender // camera stuff Camera* getCamera() { return mCamera.get(); } - const osg::Vec3f& getCameraPosition() const { return mCurrentCameraPos; } /// temporarily override the field of view with given value. void overrideFieldOfView(float val); @@ -262,7 +263,6 @@ namespace MWRender Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; osg::ref_ptr mSunLight; @@ -287,8 +287,6 @@ namespace MWRender osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; - std::unique_ptr mViewOverShoulderController; - osg::Vec3f mCurrentCameraPos; osg::ref_ptr mStateUpdater; osg::ref_ptr mSharedUniformStateUpdater; diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index fdfc3db63d..282362ea56 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include "vismask.hpp" @@ -56,13 +56,13 @@ namespace stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); - auto depth = SceneUtil::createDepth(); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); osg::ref_ptr polygonOffset (new osg::PolygonOffset); - polygonOffset->setUnits(SceneUtil::getReverseZ() ? 1 : -1); - polygonOffset->setFactor(SceneUtil::getReverseZ() ? 1 : -1); + polygonOffset->setUnits(SceneUtil::AutoDepth::isReversed() ? 1 : -1); + polygonOffset->setFactor(SceneUtil::AutoDepth::isReversed() ? 1 : -1); stateset->setAttributeAndModes(polygonOffset, osg::StateAttribute::ON); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index 5a047a1566..1973ca4025 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include @@ -251,14 +251,13 @@ namespace MWRender osg::ref_ptr screenshotCamera(new osg::Camera); osg::ref_ptr quad(new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0), 2.0))); - quad->getOrCreateStateSet()->setRenderBinDetails(100, "RenderBin", osg::StateSet::USE_RENDERBIN_DETAILS); std::map defineMap; Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr fragmentShader(shaderMgr.getShader("s360_fragment.glsl", defineMap,osg::Shader::FRAGMENT)); osg::ref_ptr vertexShader(shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr stateset = quad->getOrCreateStateSet(); osg::ref_ptr program(new osg::Program); program->addShader(fragmentShader); @@ -269,9 +268,6 @@ namespace MWRender stateset->addUniform(new osg::Uniform("mapping", screenshotMapping)); stateset->setTextureAttributeAndModes(0, cubeTexture, osg::StateAttribute::ON); - quad->setStateSet(stateset); - quad->setUpdateCallback(nullptr); - screenshotCamera->addChild(quad); renderCameraToImage(screenshotCamera, image, screenshotW, screenshotH); @@ -335,7 +331,7 @@ namespace MWRender float nearClip = Settings::Manager::getFloat("near clip", "Camera"); float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); // each cubemap side sees 90 degrees - if (SceneUtil::getReverseZ()) + if (SceneUtil::AutoDepth::isReversed()) rttCamera->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspectiveInf(90.0, w/float(h), nearClip)); else rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance); @@ -347,7 +343,7 @@ namespace MWRender rttCamera->addChild(mWater->getReflectionNode()); rttCamera->addChild(mWater->getRefractionNode()); - rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); + rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & ~(Mask_GUI|Mask_FirstPerson)); rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index dd62d6678b..c0a071c062 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,1306 +1,185 @@ #include "sky.hpp" -#include - -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include #include #include -#include -#include -#include -#include - -#include #include +#include -#include +#include -#include +#include +#include +#include +#include #include #include #include -#include - -#include -#include -#include -#include -#include -#include -#include +#include +#include #include +#include "../mwworld/weather.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "vismask.hpp" #include "renderbin.hpp" #include "util.hpp" +#include "skyutil.hpp" namespace { - osg::ref_ptr createAlphaTrackingUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::DIFFUSE); - return mat; - } - - osg::ref_ptr createUnlitMaterial() - { - osg::ref_ptr mat = new osg::Material; - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); - mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); - mat->setColorMode(osg::Material::OFF); - return mat; - } - - osg::ref_ptr createTexturedQuad(int numUvSets=1, float scale=1.f) - { - osg::ref_ptr geom = new osg::Geometry; - - osg::ref_ptr verts = new osg::Vec3Array; - verts->push_back(osg::Vec3f(-0.5*scale, -0.5*scale, 0)); - verts->push_back(osg::Vec3f(-0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, 0.5*scale, 0)); - verts->push_back(osg::Vec3f(0.5*scale, -0.5*scale, 0)); - - geom->setVertexArray(verts); - - osg::ref_ptr texcoords = new osg::Vec2Array; - texcoords->push_back(osg::Vec2f(0, 0)); - texcoords->push_back(osg::Vec2f(0, 1)); - texcoords->push_back(osg::Vec2f(1, 1)); - texcoords->push_back(osg::Vec2f(1, 0)); - - osg::ref_ptr colors = new osg::Vec4Array; - colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); - geom->setColorArray(colors, osg::Array::BIND_OVERALL); - - for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); - - geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); - - return geom; - } - -} - -namespace MWRender -{ - -class AtmosphereUpdater : public SceneUtil::StateSetUpdater -{ -public: - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - } - -private: - osg::Vec4f mEmissionColor; -}; - -class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater -{ -public: - AtmosphereNightUpdater(Resource::ImageManager* imageManager) - { - // we just need a texture, its contents don't really matter - mTexture = new osg::Texture2D(imageManager->getWarningImage()); - } - - void setFade(const float fade) - { - mColor.a() = fade; - } - -protected: - void setDefaults(osg::StateSet* stateset) override - { - osg::ref_ptr texEnv (new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - - stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override - { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mColor); - } - - osg::ref_ptr mTexture; - - osg::Vec4f mColor; -}; - -class CloudUpdater : public SceneUtil::StateSetUpdater -{ -public: - CloudUpdater() - : mAnimationTimer(0.f) - , mOpacity(0.f) - { - } - - void setAnimationTimer(float timer) - { - mAnimationTimer = timer; - } - - void setTexture(osg::ref_ptr texture) - { - mTexture = texture; - } - void setEmissionColor(const osg::Vec4f& emissionColor) - { - mEmissionColor = emissionColor; - } - void setOpacity(float opacity) - { - mOpacity = opacity; - } - -protected: - void setDefaults(osg::StateSet *stateset) override - { - osg::ref_ptr texmat (new osg::TexMat); - stateset->setTextureAttributeAndModes(0, texmat, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(1, texmat, osg::StateAttribute::ON); - stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already - osg::ref_ptr texEnvCombine (new osg::TexEnvCombine); - texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); - texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); - - stateset->setTextureAttributeAndModes(1, texEnvCombine, osg::StateAttribute::ON); - - stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override - { - osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); - texMat->setMatrix(osg::Matrix::translate(osg::Vec3f(0, -mAnimationTimer, 0.f))); - - stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); - - osg::TexEnvCombine* texEnvCombine = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); - } - -private: - float mAnimationTimer; - osg::ref_ptr mTexture; - osg::Vec4f mEmissionColor; - float mOpacity; -}; - -/// Transform that removes the eyepoint of the modelview matrix, -/// i.e. its children are positioned relative to the camera. -class CameraRelativeTransform : public osg::Transform -{ -public: - CameraRelativeTransform() - { - // Culling works in node-local space, not in camera space, so we can't cull this node correctly - // That's not a problem though, children of this node can be culled just fine - // Just make sure you do not place a CameraRelativeTransform deep in the scene graph - setCullingActive(false); - - addCullCallback(new CullCallback); - } - - CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) - : osg::Transform(copy, copyop) - { - } - - META_Node(MWRender, CameraRelativeTransform) - - const osg::Vec3f& getLastViewPoint() const - { - return mViewPoint; - } - - bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override - { - if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) - { - mViewPoint = static_cast(nv)->getViewPoint(); - } - - if (_referenceFrame==RELATIVE_RF) - { - matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); - return false; - } - else // absolute - { - matrix.makeIdentity(); - return true; - } - } - - osg::BoundingSphere computeBound() const override - { - return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); - } - - class CullCallback : public SceneUtil::NodeCallback - { - public: - void operator() (osg::Node* node, osgUtil::CullVisitor* cv) - { - // XXX have to remove unwanted culling plane of the water reflection camera - - // Remove all planes that aren't from the standard frustum - unsigned int numPlanes = 4; - if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) - ++numPlanes; - if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) - ++numPlanes; - - unsigned int mask = 0x1; - unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); - for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) - { - if (i >= numPlanes) - { - // turn off this culling plane - resultMask &= (~mask); - } - - mask <<= 1; - } - - cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); - cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); - - cv->getProjectionCullingStack().back().pushCurrentMask(); - cv->getCurrentCullingSet().pushCurrentMask(); - - traverse(node, cv); - - cv->getProjectionCullingStack().back().popCurrentMask(); - cv->getCurrentCullingSet().popCurrentMask(); - } - }; -private: - // viewPoint for the current frame - mutable osg::Vec3f mViewPoint; -}; - -class ModVertexAlphaVisitor : public osg::NodeVisitor -{ -public: - ModVertexAlphaVisitor(int meshType) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mMeshType(meshType) - { - } - - void apply(osg::Drawable& drw) override - { - osg::Geometry* geom = drw.asGeometry(); - if (!geom) - return; - - osg::ref_ptr colors = new osg::Vec4Array(geom->getVertexArray()->getNumElements()); - for (unsigned int i=0; isize(); ++i) - { - float alpha = 1.f; - if (mMeshType == 0) alpha = (i%2) ? 0.f : 1.f; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (mMeshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0.f; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 0.25098; // second row - else alpha = 1.f; - } - else if (mMeshType == 2) - { - if (geom->getColorArray()) - { - osg::Vec4Array* origColors = static_cast(geom->getColorArray()); - alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; - } - else - alpha = 1.f; - } - - (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); - } - - geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); - } - -private: - int mMeshType; -}; - -/// @brief Hides the node subgraph if the eye point is below water. -/// @note Must be added as cull callback. -/// @note Meant to be used on a node that is child of a CameraRelativeTransform. -/// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. -class UnderwaterSwitchCallback : public SceneUtil::NodeCallback -{ -public: - UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) - : mCameraRelativeTransform(cameraRelativeTransform) - , mEnabled(true) - , mWaterLevel(0.f) - { - } - - bool isUnderwater() - { - osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); - return mEnabled && viewPoint.z() < mWaterLevel; - } - - void operator()(osg::Node* node, osg::NodeVisitor* nv) - { - if (isUnderwater()) - return; - - traverse(node, nv); - } - - void setEnabled(bool enabled) - { - mEnabled = enabled; - } - void setWaterLevel(float waterLevel) - { - mWaterLevel = waterLevel; - } - -private: - osg::ref_ptr mCameraRelativeTransform; - bool mEnabled; - float mWaterLevel; -}; - -/// A base class for the sun and moons. -class CelestialBody -{ -public: - CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u) - : mVisibleMask(visibleMask) - { - mGeom = createTexturedQuad(numUvSets); - mTransform = new osg::PositionAttitudeTransform; - mTransform->setNodeMask(mVisibleMask); - mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); - mTransform->addChild(mGeom); - - parentNode->addChild(mTransform); - } - - virtual ~CelestialBody() {} - - virtual void adjustTransparency(const float ratio) = 0; - - void setVisible(bool visible) - { - mTransform->setNodeMask(visible ? mVisibleMask : 0); - } - -protected: - unsigned int mVisibleMask; - static const float mDistance; - osg::ref_ptr mTransform; - osg::ref_ptr mGeom; -}; - -const float CelestialBody::mDistance = 1000.0f; - -class Sun : public CelestialBody -{ -public: - Sun(osg::Group* parentNode, Resource::ImageManager& imageManager) - : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) - , mUpdater(new Updater) - { - mTransform->addUpdateCallback(mUpdater); - - osg::ref_ptr sunTex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds"))); - sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - - osg::ref_ptr queryNode (new osg::Group); - // Need to render after the world geometry so we can correctly test for occlusions - osg::StateSet* stateset = queryNode->getOrCreateStateSet(); - stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); - stateset->setNestRenderBins(false); - // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun - osg::ref_ptr alphaFunc (new osg::AlphaFunc); - alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); - stateset->setTextureAttributeAndModes(0, sunTex, osg::StateAttribute::ON); - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); - // Disable writing to the color buffer. We are using this geometry for visibility tests only. - osg::ref_ptr colormask (new osg::ColorMask(0, 0, 0, 0)); - stateset->setAttributeAndModes(colormask, osg::StateAttribute::ON); - - mTransform->addChild(queryNode); - - mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); - mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); - - createSunFlash(imageManager); - createSunGlare(); - } - - ~Sun() - { - mTransform->removeUpdateCallback(mUpdater); - destroySunFlash(); - destroySunGlare(); - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mColor.r() = color.r(); - mUpdater->mColor.g() = color.g(); - mUpdater->mColor.b() = color.b(); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mColor.a() = ratio; - if (mSunGlareCallback) - mSunGlareCallback->setGlareView(ratio); - if (mSunFlashCallback) - mSunFlashCallback->setGlareView(ratio); - } - - void setDirection(const osg::Vec3f& direction) - { - osg::Vec3f normalizedDirection = direction / direction.length(); - mTransform->setPosition(normalizedDirection * mDistance); - - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); - mTransform->setAttitude(quat); - } - - void setGlareTimeOfDayFade(float val) - { - if (mSunGlareCallback) - mSunGlareCallback->setTimeOfDayFade(val); - } - -private: - class DummyComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback - { - public: - osg::BoundingSphere computeBound(const osg::Node& node) const override { return osg::BoundingSphere(); } - }; - - /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. - osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible) - { - osg::ref_ptr oqn = new osg::OcclusionQueryNode; - oqn->setQueriesEnabled(true); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced - osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); -#else - osg::ref_ptr queryGeom = oqn->getQueryGeometry(); -#endif - - // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, - // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry - // is only called once. - // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. - queryGeom->setDataVariance(osg::Object::STATIC); - - // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, - // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to - // circumvent this. - queryGeom->setVertexArray(mGeom->getVertexArray()); - queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); - queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); - queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); - - // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. - oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); - // Still need a proper bounding sphere. - oqn->setInitialBound(queryGeom->getBound()); - -#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) - oqn->setQueryGeometry(queryGeom.release()); -#endif - - osg::StateSet* queryStateSet = new osg::StateSet; - if (queryVisible) - { - auto depth = SceneUtil::createDepth(); - // This is a trick to make fragments written by the query always use the maximum depth value, - // without having to retrieve the current far clipping distance. - // We want the sun glare to be "infinitely" far away. - double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; - depth->setZNear(far); - depth->setZFar(far); - depth->setWriteMask(false); - queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - else - { - queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - } - oqn->setQueryStateSet(queryStateSet); - - parent->addChild(oqn); - - return oqn; - } - - void createSunFlash(Resource::ImageManager& imageManager) - { - osg::ref_ptr tex (new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds"))); - tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - osg::ref_ptr group (new osg::Group); - - mTransform->addChild(group); - - const float scale = 2.6f; - osg::ref_ptr geom = createTexturedQuad(1, scale); - group->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setTextureAttributeAndModes(0, tex, osg::StateAttribute::ON); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - - mSunFlashNode = group; - - mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); - mSunFlashNode->addCullCallback(mSunFlashCallback); - } - void destroySunFlash() - { - if (mSunFlashNode) - { - mSunFlashNode->removeCullCallback(mSunFlashCallback); - mSunFlashCallback = nullptr; - } - } - - void createSunGlare() - { - osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? - camera->setViewMatrix(osg::Matrix::identity()); - camera->setClearMask(0); - camera->setRenderOrder(osg::Camera::NESTED_RENDER); - camera->setAllowEventFocus(false); - - osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); - - camera->addChild(geom); - - osg::StateSet* stateset = geom->getOrCreateStateSet(); - - stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); - stateset->setNestRenderBins(false); - stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); - - // set up additive blending - osg::ref_ptr blendFunc (new osg::BlendFunc); - blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); - blendFunc->setDestination(osg::BlendFunc::ONE); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); - - mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); - mSunGlareNode = camera; - - mSunGlareNode->addCullCallback(mSunGlareCallback); - - mTransform->addChild(camera); - } - void destroySunGlare() - { - if (mSunGlareNode) - { - mSunGlareNode->removeCullCallback(mSunGlareCallback); - mSunGlareCallback = nullptr; - } - } - - class Updater : public SceneUtil::StateSetUpdater + class WrapAroundOperator : public osgParticle::Operator { public: - osg::Vec4f mColor; - - Updater() - : mColor(1.f, 1.f, 1.f, 1.f) + WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange) + : osgParticle::Operator() + , mCamera(camera) + , mWrapRange(wrapRange) + , mHalfWrapRange(mWrapRange / 2.0) { + mPreviousCameraPosition = getCameraPosition(); } - void setDefaults(osg::StateSet* stateset) override + osg::Object *cloneType() const override { - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON); + return nullptr; } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + osg::Object *clone(const osg::CopyOp &op) const override { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); - mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + return nullptr; } - }; - class OcclusionCallback - { - public: - OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : mOcclusionQueryVisiblePixels(oqnVisible) - , mOcclusionQueryTotalPixels(oqnTotal) + void operate(osgParticle::Particle *P, double dt) override { } - protected: - float getVisibleRatio (osg::Camera* camera) + void operateParticles(osgParticle::ParticleSystem *ps, double dt) override { - int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); - int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); - - float visibleRatio = 0.f; - if (total > 0) - visibleRatio = static_cast(visible) / static_cast(total); - - float dt = MWBase::Environment::get().getFrameDuration(); - - float lastRatio = mLastRatio[osg::observer_ptr(camera)]; - - float change = dt*10; - - if (visibleRatio > lastRatio) - visibleRatio = std::min(visibleRatio, lastRatio + change); - else - visibleRatio = std::max(visibleRatio, lastRatio - change); + osg::Vec3 position = getCameraPosition(); + osg::Vec3 positionDifference = position - mPreviousCameraPosition; - mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + osg::Matrix toWorld, toLocal; - return visibleRatio; - } - - private: - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; + std::vector worldMatrices = ps->getWorldMatrices(); - std::map, float> mLastRatio; - }; - /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. - class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback - { - public: - SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) - : OcclusionCallback(oqnVisible, oqnTotal) - , mGlareView(1.f) - { - } - - void operator()(osg::Node* node, osgUtil::CullVisitor* cv) - { - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - osg::ref_ptr stateset; - - if (visibleRatio > 0.f) + if (!worldMatrices.empty()) { - const float fadeThreshold = 0.1; - if (visibleRatio < fadeThreshold) - { - float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; - osg::ref_ptr mat (createUnlitMaterial()); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); - stateset = new osg::StateSet; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } - else if (visibleRatio < 1.f) - { - const float threshold = 0.6; - visibleRatio = visibleRatio * (1.f - threshold) + threshold; - } + toWorld = worldMatrices[0]; + toLocal.invert(toWorld); } - float scale = visibleRatio; - - if (scale == 0.f) - { - // no traverse - return; - } - else if (scale == 1.f) - traverse(node, cv); - else + for (int i = 0; i < ps->numParticles(); ++i) { - osg::Matrix modelView = *cv->getModelViewMatrix(); + osgParticle::Particle *p = ps->getParticle(i); + p->setPosition(toWorld.preMult(p->getPosition())); + p->setPosition(p->getPosition() - positionDifference); - modelView.preMultScale(osg::Vec3f(scale, scale, scale)); - - if (stateset) - cv->pushStateSet(stateset); - - cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); - - traverse(node, cv); - - cv->popModelViewMatrix(); - - if (stateset) - cv->popStateSet(); - } - } - - void setGlareView(float value) - { - mGlareView = value; - } - - private: - float mGlareView; - }; - - - /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. - /// Must be attached as a cull callback to the node above the glare node. - class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback - { - public: - SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, - osg::ref_ptr sunTransform) - : OcclusionCallback(oqnVisible, oqnTotal) - , mSunTransform(sunTransform) - , mTimeOfDayFade(1.f) - , mGlareView(1.f) - { - mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); - mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); - mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); - - // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, - // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, - // so the resulting color looks more orange than red. - mColor *= 2; - for (int i=0; i<3; ++i) - mColor[i] = std::min(1.f, mColor[i]); - } - - void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) - { - float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); - float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); - - const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); - - float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); - float fade = value * mSunGlareFaderMax; - - fade *= mTimeOfDayFade * mGlareView * visibleRatio; - - if (fade == 0.f) - { - // no traverse - return; - } - else - { - osg::ref_ptr stateset (new osg::StateSet); - - osg::ref_ptr mat (createUnlitMaterial()); + for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions + { + osg::Vec3 pos = p->getPosition(); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); - mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + if (pos[j] < -mHalfWrapRange[j]) + pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); + else if (pos[j] > mHalfWrapRange[j]) + pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); + p->setPosition(pos); + } - cv->pushStateSet(stateset); - traverse(node, cv); - cv->popStateSet(); + p->setPosition(toLocal.preMult(p->getPosition())); } - } - void setTimeOfDayFade(float val) - { - mTimeOfDayFade = val; + mPreviousCameraPosition = position; } - void setGlareView(float glareView) - { - mGlareView = glareView; - } + protected: + osg::Camera *mCamera; + osg::Vec3 mPreviousCameraPosition; + osg::Vec3 mWrapRange; + osg::Vec3 mHalfWrapRange; - private: - float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + osg::Vec3 getCameraPosition() { - osg::Vec3d eye, center, up; - viewMatrix.getLookAt(eye, center, up); - - osg::Vec3d forward = center - eye; - osg::Vec3d sun = mSunTransform->getPosition(); - - forward.normalize(); - sun.normalize(); - float angleRadians = std::acos(forward * sun); - return angleRadians; + return mCamera->getInverseViewMatrix().getTrans(); } - - osg::ref_ptr mSunTransform; - float mTimeOfDayFade; - float mGlareView; - osg::Vec4f mColor; - float mSunGlareFaderMax; - float mSunGlareFaderAngleMax; }; - osg::ref_ptr mUpdater; - osg::ref_ptr mSunFlashCallback; - osg::ref_ptr mSunFlashNode; - osg::ref_ptr mSunGlareCallback; - osg::ref_ptr mSunGlareNode; - osg::ref_ptr mOcclusionQueryVisiblePixels; - osg::ref_ptr mOcclusionQueryTotalPixels; -}; - -class Moon : public CelestialBody -{ -public: - enum Type - { - Type_Masser = 0, - Type_Secunda - }; - - Moon(osg::Group* parentNode, Resource::ImageManager& imageManager, float scaleFactor, Type type) - : CelestialBody(parentNode, scaleFactor, 2) - , mType(type) - , mPhase(MoonState::Phase::Unspecified) - , mUpdater(new Updater(imageManager)) - { - setPhase(MoonState::Phase::Full); - setVisible(true); - - mGeom->addUpdateCallback(mUpdater); - } - - ~Moon() - { - mGeom->removeUpdateCallback(mUpdater); - } - - void adjustTransparency(const float ratio) override - { - mUpdater->mTransparency *= ratio; - } - - void setState(const MoonState& state) - { - float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; - float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; - - osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); - - osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); - mTransform->setPosition(direction * mDistance); - - // The moon quad is initially oriented facing down, so we need to offset its X-axis - // rotation to rotate it to face the camera when sitting at the horizon. - osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); - mTransform->setAttitude(attX * rotZ); - - setPhase(state.mPhase); - mUpdater->mTransparency = state.mMoonAlpha; - mUpdater->mShadowBlend = state.mShadowBlend; - } - - void setAtmosphereColor(const osg::Vec4f& color) - { - mUpdater->mAtmosphereColor = color; - } - - void setColor(const osg::Vec4f& color) - { - mUpdater->mMoonColor = color; - } - - unsigned int getPhaseInt() const + class WeatherAlphaOperator : public osgParticle::Operator { - if (mPhase == MoonState::Phase::New) return 0; - else if (mPhase == MoonState::Phase::WaxingCrescent) return 1; - else if (mPhase == MoonState::Phase::WaningCrescent) return 1; - else if (mPhase == MoonState::Phase::FirstQuarter) return 2; - else if (mPhase == MoonState::Phase::ThirdQuarter) return 2; - else if (mPhase == MoonState::Phase::WaxingGibbous) return 3; - else if (mPhase == MoonState::Phase::WaningGibbous) return 3; - else if (mPhase == MoonState::Phase::Full) return 4; - return 0; - } + public: + WeatherAlphaOperator(float& alpha, bool rain) + : mAlpha(alpha) + , mIsRain(rain) + { } -private: - struct Updater : public SceneUtil::StateSetUpdater - { - Resource::ImageManager& mImageManager; - osg::ref_ptr mPhaseTex; - osg::ref_ptr mCircleTex; - float mTransparency; - float mShadowBlend; - osg::Vec4f mAtmosphereColor; - osg::Vec4f mMoonColor; - - Updater(Resource::ImageManager& imageManager) - : mImageManager(imageManager) - , mPhaseTex() - , mCircleTex() - , mTransparency(1.0f) - , mShadowBlend(1.0f) - , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) - , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + osg::Object *cloneType() const override { + return nullptr; } - void setDefaults(osg::StateSet* stateset) override + osg::Object *clone(const osg::CopyOp &op) const override { - stateset->setTextureAttributeAndModes(0, mPhaseTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); - texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor - stateset->setTextureAttributeAndModes(0, texEnv, osg::StateAttribute::ON); - - stateset->setTextureAttributeAndModes(1, mCircleTex, osg::StateAttribute::ON); - osg::ref_ptr texEnv2 = new osg::TexEnvCombine; - texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); - texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); - texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); - texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); - texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency - stateset->setTextureAttributeAndModes(1, texEnv2, osg::StateAttribute::ON); - - stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + return nullptr; } - void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + void operate(osgParticle::Particle *particle, double dt) override { - osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); - texEnv->setConstantColor(mMoonColor * mShadowBlend); - - osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); - texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); - } - - void setTextures(const std::string& phaseTex, const std::string& circleTex) - { - mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); - mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); - mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - reset(); - } - }; - - Type mType; - MoonState::Phase mPhase; - osg::ref_ptr mUpdater; - - void setPhase(const MoonState::Phase& phase) - { - if(mPhase == phase) - return; - - mPhase = phase; - - std::string textureName = "textures/tx_"; - - if (mType == Moon::Type_Secunda) - textureName += "secunda_"; - else - textureName += "masser_"; - - if (phase == MoonState::Phase::New) textureName += "new"; - else if(phase == MoonState::Phase::WaxingCrescent) textureName += "one_wax"; - else if(phase == MoonState::Phase::FirstQuarter) textureName += "half_wax"; - else if(phase == MoonState::Phase::WaxingGibbous) textureName += "three_wax"; - else if(phase == MoonState::Phase::WaningCrescent) textureName += "one_wan"; - else if(phase == MoonState::Phase::ThirdQuarter) textureName += "half_wan"; - else if(phase == MoonState::Phase::WaningGibbous) textureName += "three_wan"; - else if(phase == MoonState::Phase::Full) textureName += "full"; - - textureName += ".dds"; - - if (mType == Moon::Type_Secunda) - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); - else - mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); - } -}; - -SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) - : mSceneManager(sceneManager) - , mCamera(nullptr) - , mAtmosphereNightRoll(0.f) - , mCreated(false) - , mIsStorm(false) - , mDay(0) - , mMonth(0) - , mCloudAnimationTimer(0.f) - , mRainTimer(0.f) - , mStormDirection(0,1,0) - , mClouds() - , mNextClouds() - , mCloudBlendFactor(0.0f) - , mCloudSpeed(0.0f) - , mStarsOpacity(0.0f) - , mRemainingTransitionTime(0.0f) - , mRainEnabled(false) - , mRainSpeed(0) - , mRainDiameter(0) - , mRainMinHeight(0) - , mRainMaxHeight(0) - , mRainEntranceSpeed(1) - , mRainMaxRaindrops(0) - , mWindSpeed(0.f) - , mBaseWindSpeed(0.f) - , mEnabled(true) - , mSunEnabled(true) - , mPrecipitationAlpha(0.f) -{ - osg::ref_ptr skyroot (new CameraRelativeTransform); - skyroot->setName("Sky Root"); - // Assign empty program to specify we don't want shaders - // The shaders generated by the SceneManager can't handle everything we need - skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); - SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - - skyroot->setNodeMask(Mask_Sky); - parentNode->addChild(skyroot); - - mRootNode = skyroot; - - mEarlyRenderBinRoot = new osg::Group; - // render before the world is rendered - mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); - // Prevent unwanted clipping by water reflection camera's clipping plane - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); - mRootNode->addChild(mEarlyRenderBinRoot); - - mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); -} - -void SkyManager::create() -{ - assert(!mCreated); - - mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); - ModVertexAlphaVisitor modAtmosphere(0); - mAtmosphereDay->accept(modAtmosphere); - - mAtmosphereUpdater = new AtmosphereUpdater; - mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); - - mAtmosphereNightNode = new osg::PositionAttitudeTransform; - mAtmosphereNightNode->setNodeMask(0); - mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); - - osg::ref_ptr atmosphereNight; - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); - else - atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); - atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - ModVertexAlphaVisitor modStars(2); - atmosphereNight->accept(modStars); - mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager()); - atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); - - mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager->getImageManager())); - - mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); - mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager->getImageManager(), Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); - - mCloudNode = new osg::PositionAttitudeTransform; - mEarlyRenderBinRoot->addChild(mCloudNode); - mCloudMesh = mSceneManager->getInstance(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(Settings::Manager::getString("skyclouds", "Models"), mCloudNode); - mCloudMesh2->accept(modClouds); - mCloudUpdater2 = new CloudUpdater; - mCloudUpdater2->setOpacity(0.f); - mCloudMesh2->addUpdateCallback(mCloudUpdater2); - mCloudMesh2->setNodeMask(0); - - auto depth = SceneUtil::createDepth(); - depth->setWriteMask(false); - mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); - mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); - - mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); - - mCreated = true; -} - -class RainCounter : public osgParticle::ConstantRateCounter -{ -public: - int numParticlesToCreate(double dt) const override - { - // limit dt to avoid large particle emissions if there are jumps in the simulation time - // 0.2 seconds is the same cap as used in Engine's frame loop - dt = std::min(dt, 0.2); - return ConstantRateCounter::numParticlesToCreate(dt); - } -}; - -class RainShooter : public osgParticle::Shooter -{ -public: - RainShooter() - : mAngle(0.f) - { - } - - void shoot(osgParticle::Particle* particle) const override - { - particle->setVelocity(mVelocity); - particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); - } - - void setVelocity(const osg::Vec3f& velocity) - { - mVelocity = velocity; - } - - void setAngle(float angle) - { - mAngle = angle; - } - - osg::Object* cloneType() const override - { - return new RainShooter; - } - osg::Object* clone(const osg::CopyOp &) const override - { - return new RainShooter(*this); - } + constexpr float rainThreshold = 0.6f; // Rain_Threshold? + float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; + particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); + } -private: - osg::Vec3f mVelocity; - float mAngle; -}; + private: + float &mAlpha; + bool mIsRain; + }; -// Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. -class AlphaFader : public SceneUtil::StateSetUpdater -{ -public: - /// @param alpha the variable alpha value is recovered from - AlphaFader(float& alpha) - : mAlpha(alpha) + // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. + class AlphaFader : public SceneUtil::StateSetUpdater { - } + public: + /// @param alpha the variable alpha value is recovered from + AlphaFader(const float& alpha) + : mAlpha(alpha) + { } - void setDefaults(osg::StateSet* stateset) override - { - // need to create a deep copy of StateAttributes we will modify - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); - } + void setDefaults(osg::StateSet* stateset) override + { + // need to create a deep copy of StateAttributes we will modify + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); + } - void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override - { - osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mAlpha)); - } + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, mAlpha)); + } + + protected: + const float &mAlpha; + }; // Helper for adding AlphaFaders to a subgraph class SetupVisitor : public osg::NodeVisitor { public: - SetupVisitor(float &alpha) + SetupVisitor(const float &alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) - { - } + { } void apply(osg::Node &node) override { @@ -1320,7 +199,7 @@ public: callback = callback->getNestedCallback(); } - osg::ref_ptr alphaFader (new AlphaFader(mAlpha)); + osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); @@ -1333,608 +212,666 @@ public: } private: - float &mAlpha; + const float &mAlpha; }; - -protected: - float &mAlpha; -}; - -void SkyManager::setCamera(osg::Camera *camera) -{ - mCamera = camera; } -class WrapAroundOperator : public osgParticle::Operator +namespace MWRender { -public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator(), - mCamera(camera), mWrapRange(wrapRange), mHalfWrapRange(mWrapRange / 2.0) + SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneManager) + : mSceneManager(sceneManager) + , mCamera(nullptr) + , mAtmosphereNightRoll(0.f) + , mCreated(false) + , mIsStorm(false) + , mDay(0) + , mMonth(0) + , mCloudAnimationTimer(0.f) + , mRainTimer(0.f) + , mStormParticleDirection(MWWorld::Weather::defaultDirection()) + , mStormDirection(MWWorld::Weather::defaultDirection()) + , mClouds() + , mNextClouds() + , mCloudBlendFactor(0.f) + , mCloudSpeed(0.f) + , mStarsOpacity(0.f) + , mRemainingTransitionTime(0.f) + , mRainEnabled(false) + , mRainSpeed(0.f) + , mRainDiameter(0.f) + , mRainMinHeight(0.f) + , mRainMaxHeight(0.f) + , mRainEntranceSpeed(1.f) + , mRainMaxRaindrops(0) + , mWindSpeed(0.f) + , mBaseWindSpeed(0.f) + , mEnabled(true) + , mSunEnabled(true) + , mPrecipitationAlpha(0.f) { - mPreviousCameraPosition = getCameraPosition(); - } + osg::ref_ptr skyroot = new CameraRelativeTransform; + skyroot->setName("Sky Root"); + // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline + if (!mSceneManager->getForceShaders()) + skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE|osg::StateAttribute::PROTECTED|osg::StateAttribute::ON); + SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet()); - osg::Object *cloneType() const override - { - return nullptr; + skyroot->setNodeMask(Mask_Sky); + parentNode->addChild(skyroot); + + mRootNode = skyroot; + + mEarlyRenderBinRoot = new osg::Group; + // render before the world is rendered + mEarlyRenderBinRoot->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + // Prevent unwanted clipping by water reflection camera's clipping plane + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); + mRootNode->addChild(mEarlyRenderBinRoot); + + mUnderwaterSwitch = new UnderwaterSwitchCallback(skyroot); } - osg::Object *clone(const osg::CopyOp &op) const override + void SkyManager::create() { - return nullptr; + assert(!mCreated); + + bool forceShaders = mSceneManager->getForceShaders(); + + mAtmosphereDay = mSceneManager->getInstance(Settings::Manager::getString("skyatmosphere", "Models"), mEarlyRenderBinRoot); + ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); + mAtmosphereDay->accept(modAtmosphere); + + mAtmosphereUpdater = new AtmosphereUpdater; + mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); + + mAtmosphereNightNode = new osg::PositionAttitudeTransform; + mAtmosphereNightNode->setNodeMask(0); + mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); + + osg::ref_ptr atmosphereNight; + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight02", "Models"), mAtmosphereNightNode); + else + atmosphereNight = mSceneManager->getInstance(Settings::Manager::getString("skynight01", "Models"), mAtmosphereNightNode); + atmosphereNight->getOrCreateStateSet()->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); + atmosphereNight->accept(modStars); + mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); + atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); + + mSun.reset(new Sun(mEarlyRenderBinRoot, *mSceneManager)); + mMasser.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Masser_Size")/125, Moon::Type_Masser)); + mSecunda.reset(new Moon(mEarlyRenderBinRoot, *mSceneManager, Fallback::Map::getFloat("Moons_Secunda_Size")/125, Moon::Type_Secunda)); + + mCloudNode = new osg::Group; + mEarlyRenderBinRoot->addChild(mCloudNode); + + mCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mCloudMesh); + mCloudUpdater = new CloudUpdater(forceShaders); + mCloudUpdater->setOpacity(1.f); + cloudMeshChild->addUpdateCallback(mCloudUpdater); + mCloudMesh->addChild(cloudMeshChild); + + mNextCloudMesh = new osg::PositionAttitudeTransform; + osg::ref_ptr nextCloudMeshChild = mSceneManager->getInstance(Settings::Manager::getString("skyclouds", "Models"), mNextCloudMesh); + mNextCloudUpdater = new CloudUpdater(forceShaders); + mNextCloudUpdater->setOpacity(0.f); + nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); + mNextCloudMesh->setNodeMask(0); + mNextCloudMesh->addChild(nextCloudMeshChild); + + mCloudNode->addChild(mCloudMesh); + mCloudNode->addChild(mNextCloudMesh); + + ModVertexAlphaVisitor modClouds(ModVertexAlphaVisitor::Clouds); + mCloudMesh->accept(modClouds); + mNextCloudMesh->accept(modClouds); + + if (mSceneManager->getForceShaders()) + { + auto vertex = mSceneManager->getShaderManager().getShader("sky_vertex.glsl", {}, osg::Shader::VERTEX); + auto fragment = mSceneManager->getShaderManager().getShader("sky_fragment.glsl", {}, osg::Shader::FRAGMENT); + auto program = mSceneManager->getShaderManager().getProgram(vertex, fragment); + mEarlyRenderBinRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", -1)); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + + osg::ref_ptr depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); + mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); + + mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); + + mCreated = true; } - void operate(osgParticle::Particle *P, double dt) override + void SkyManager::setCamera(osg::Camera *camera) { + mCamera = camera; } - void operateParticles(osgParticle::ParticleSystem *ps, double dt) override + void SkyManager::createRain() { - osg::Vec3 position = getCameraPosition(); - osg::Vec3 positionDifference = position - mPreviousCameraPosition; + if (mRainNode) + return; - osg::Matrix toWorld, toLocal; + mRainNode = new osg::Group; - std::vector worldMatrices = ps->getWorldMatrices(); - - if (!worldMatrices.empty()) - { - toWorld = worldMatrices[0]; - toLocal.invert(toWorld); - } + mRainParticleSystem = new NifOsg::ParticleSystem; + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - for (int i = 0; i < ps->numParticles(); ++i) - { - osgParticle::Particle *p = ps->getParticle(i); - p->setPosition(toWorld.preMult(p->getPosition())); - p->setPosition(p->getPosition() - positionDifference); + mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); + mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); + mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions - { - osg::Vec3 pos = p->getPosition(); + osg::ref_ptr stateset = mRainParticleSystem->getOrCreateStateSet(); - if (pos[j] < -mHalfWrapRange[j]) - pos[j] = mHalfWrapRange[j] + fmod(pos[j] - mHalfWrapRange[j],mWrapRange[j]); - else if (pos[j] > mHalfWrapRange[j]) - pos[j] = fmod(pos[j] + mHalfWrapRange[j],mWrapRange[j]) - mHalfWrapRange[j]; + osg::ref_ptr raindropTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds")); + raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - p->setPosition(pos); - } + stateset->setTextureAttributeAndModes(0, raindropTex); + stateset->setNestRenderBins(false); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - p->setPosition(toLocal.preMult(p->getPosition())); - } + osg::ref_ptr mat = new osg::Material; + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(mat); - mPreviousCameraPosition = position; - } + osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); + particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); + particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); + particleTemplate.setLifeTime(1); -protected: - osg::Camera *mCamera; - osg::Vec3 mPreviousCameraPosition; - osg::Vec3 mWrapRange; - osg::Vec3 mHalfWrapRange; + osg::ref_ptr emitter = new osgParticle::ModularEmitter; + emitter->setParticleSystem(mRainParticleSystem); - osg::Vec3 getCameraPosition() - { - return mCamera->getInverseViewMatrix().getTrans(); + osg::ref_ptr placer = new osgParticle::BoxPlacer; + placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + emitter->setPlacer(placer); + mPlacer = placer; + + // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. + // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). + // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. + osg::ref_ptr counter = new RainCounter; + counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + emitter->setCounter(counter); + mCounter = counter; + + osg::ref_ptr shooter = new RainShooter; + mRainShooter = shooter; + emitter->setShooter(shooter); + + osg::ref_ptr updater = new osgParticle::ParticleSystemUpdater; + updater->addParticleSystem(mRainParticleSystem); + + osg::ref_ptr program = new osgParticle::ModularProgram; + program->addOperator(new WrapAroundOperator(mCamera,rainRange)); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); + program->setParticleSystem(mRainParticleSystem); + mRainNode->addChild(program); + + mRainNode->addChild(emitter); + mRainNode->addChild(mRainParticleSystem); + mRainNode->addChild(updater); + + // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. + mRainNode->addCullCallback(mUnderwaterSwitch); + mRainNode->setNodeMask(Mask_WeatherParticles); + + mRainParticleSystem->setUserValue("simpleLighting", true); + mSceneManager->recreateShaders(mRainNode); + + mRootNode->addChild(mRainNode); } -}; -class WeatherAlphaOperator : public osgParticle::Operator -{ -public: - WeatherAlphaOperator(float& alpha, bool rain) - : mAlpha(alpha) - , mIsRain(rain) + void SkyManager::destroyRain() { + if (!mRainNode) + return; + + mRootNode->removeChild(mRainNode); + mRainNode = nullptr; + mPlacer = nullptr; + mCounter = nullptr; + mRainParticleSystem = nullptr; + mRainShooter = nullptr; } - osg::Object *cloneType() const override + SkyManager::~SkyManager() { - return nullptr; + if (mRootNode) + { + mRootNode->getParent(0)->removeChild(mRootNode); + mRootNode = nullptr; + } } - osg::Object *clone(const osg::CopyOp &op) const override + int SkyManager::getMasserPhase() const { - return nullptr; + if (!mCreated) return 0; + return mMasser->getPhaseInt(); } - void operate(osgParticle::Particle *particle, double dt) override + int SkyManager::getSecundaPhase() const { - constexpr float rainThreshold = 0.6f; // Rain_Threshold? - const float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; - particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); + if (!mCreated) return 0; + return mSecunda->getPhaseInt(); } -private: - float &mAlpha; - bool mIsRain; -}; - -void SkyManager::createRain() -{ - if (mRainNode) - return; - - mRainNode = new osg::Group; - - mRainParticleSystem = new NifOsg::ParticleSystem; - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); - - mRainParticleSystem->setParticleAlignment(osgParticle::ParticleSystem::FIXED); - mRainParticleSystem->setAlignVectorX(osg::Vec3f(0.1,0,0)); - mRainParticleSystem->setAlignVectorY(osg::Vec3f(0,0,1)); - - osg::ref_ptr stateset (mRainParticleSystem->getOrCreateStateSet()); - - osg::ref_ptr raindropTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage("textures/tx_raindrop_01.dds"))); - raindropTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - raindropTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - stateset->setTextureAttributeAndModes(0, raindropTex, osg::StateAttribute::ON); - stateset->setNestRenderBins(false); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - osg::ref_ptr mat (new osg::Material); - mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); - mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - - osgParticle::Particle& particleTemplate = mRainParticleSystem->getDefaultParticleTemplate(); - particleTemplate.setSizeRange(osgParticle::rangef(5.f, 15.f)); - particleTemplate.setAlphaRange(osgParticle::rangef(1.f, 1.f)); - particleTemplate.setLifeTime(1); - - osg::ref_ptr emitter (new osgParticle::ModularEmitter); - emitter->setParticleSystem(mRainParticleSystem); - - osg::ref_ptr placer (new osgParticle::BoxPlacer); - placer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - placer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - placer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); - emitter->setPlacer(placer); - mPlacer = placer; - - // FIXME: vanilla engine does not use a particle system to handle rain, it uses a NIF-file with 20 raindrops in it. - // It spawns the (maxRaindrops-getParticleSystem()->numParticles())*dt/rainEntranceSpeed batches every frame (near 1-2). - // Since the rain is a regular geometry, it produces water ripples, also in theory it can be removed if collides with something. - osg::ref_ptr counter (new RainCounter); - counter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); - emitter->setCounter(counter); - mCounter = counter; - - osg::ref_ptr shooter (new RainShooter); - mRainShooter = shooter; - emitter->setShooter(shooter); - - osg::ref_ptr updater (new osgParticle::ParticleSystemUpdater); - updater->addParticleSystem(mRainParticleSystem); - - osg::ref_ptr program (new osgParticle::ModularProgram); - program->addOperator(new WrapAroundOperator(mCamera,rainRange)); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, true)); - program->setParticleSystem(mRainParticleSystem); - mRainNode->addChild(program); - - mRainNode->addChild(emitter); - mRainNode->addChild(mRainParticleSystem); - mRainNode->addChild(updater); - - // Note: if we ever switch to regular geometry rain, it'll need to use an AlphaFader. - mRainNode->addCullCallback(mUnderwaterSwitch); - mRainNode->setNodeMask(Mask_WeatherParticles); - - mRootNode->addChild(mRainNode); -} - -void SkyManager::destroyRain() -{ - if (!mRainNode) - return; - - mRootNode->removeChild(mRainNode); - mRainNode = nullptr; - mPlacer = nullptr; - mCounter = nullptr; - mRainParticleSystem = nullptr; - mRainShooter = nullptr; -} - -SkyManager::~SkyManager() -{ - if (mRootNode) + bool SkyManager::isEnabled() { - mRootNode->getParent(0)->removeChild(mRootNode); - mRootNode = nullptr; + return mEnabled; } -} - -int SkyManager::getMasserPhase() const -{ - if (!mCreated) return 0; - return mMasser->getPhaseInt(); -} - -int SkyManager::getSecundaPhase() const -{ - if (!mCreated) return 0; - return mSecunda->getPhaseInt(); -} - -bool SkyManager::isEnabled() -{ - return mEnabled; -} -bool SkyManager::hasRain() const -{ - return mRainNode != nullptr; -} - -float SkyManager::getPrecipitationAlpha() const -{ - if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) - return mPrecipitationAlpha; - - return 0.f; -} + bool SkyManager::hasRain() const + { + return mRainNode != nullptr; + } -void SkyManager::update(float duration) -{ - if (!mEnabled) - return; + float SkyManager::getPrecipitationAlpha() const + { + if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) + return mPrecipitationAlpha; - switchUnderwaterRain(); + return 0.f; + } - if (mIsStorm) + void SkyManager::update(float duration) { - osg::Quat quat; - quat.makeRotate(osg::Vec3f(0,1,0), mStormDirection); + if (!mEnabled) + return; - mCloudNode->setAttitude(quat); - if (mParticleNode) + switchUnderwaterRain(); + + if (mIsStorm && mParticleNode) { + osg::Quat quat; + quat.makeRotate(MWWorld::Weather::defaultDirection(), mStormParticleDirection); // Morrowind deliberately rotates the blizzard mesh, so so should we. if (mCurrentParticleEffect == Settings::Manager::getString("weatherblizzard", "Models")) - quat.makeRotate(osg::Vec3f(-1,0,0), mStormDirection); + quat.makeRotate(osg::Vec3f(-1,0,0), mStormParticleDirection); mParticleNode->setAttitude(quat); } - } - else - mCloudNode->setAttitude(osg::Quat()); - - // UV Scroll the clouds - mCloudAnimationTimer += duration * mCloudSpeed * 0.003; - mCloudUpdater->setAnimationTimer(mCloudAnimationTimer); - mCloudUpdater2->setAnimationTimer(mCloudAnimationTimer); - - // rotate the stars by 360 degrees every 4 days - mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); - if (mAtmosphereNightNode->getNodeMask() != 0) - mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); -} -void SkyManager::setEnabled(bool enabled) -{ - if (enabled && !mCreated) - create(); + // UV Scroll the clouds + mCloudAnimationTimer += duration * mCloudSpeed * 0.003; + mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); + mCloudUpdater->setTextureCoord(mCloudAnimationTimer); - mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); + // morrowind rotates each cloud mesh independently + osg::Quat rotation; + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); + mCloudMesh->setAttitude(rotation); - mEnabled = enabled; -} + if (mNextCloudMesh->getNodeMask()) + { + rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); + mNextCloudMesh->setAttitude(rotation); + } -void SkyManager::setMoonColour (bool red) -{ - if (!mCreated) return; - mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); -} + // rotate the stars by 360 degrees every 4 days + mAtmosphereNightRoll += MWBase::Environment::get().getWorld()->getTimeScaleFactor()*duration*osg::DegreesToRadians(360.f) / (3600*96.f); + if (mAtmosphereNightNode->getNodeMask() != 0) + mAtmosphereNightNode->setAttitude(osg::Quat(mAtmosphereNightRoll, osg::Vec3f(0,0,1))); + } -void SkyManager::updateRainParameters() -{ - if (mRainShooter) + void SkyManager::setEnabled(bool enabled) { - float angle = -std::atan(mWindSpeed/50.f); - mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); - mRainShooter->setAngle(angle); + if (enabled && !mCreated) + create(); - osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); + mRootNode->setNodeMask(enabled ? Mask_Sky : 0u); - mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); - mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); - mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + if (!enabled && mParticleNode && mParticleEffect) + { + mCurrentParticleEffect.clear(); + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } - mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); + mEnabled = enabled; } -} - -void SkyManager::switchUnderwaterRain() -{ - if (!mRainParticleSystem) - return; - bool freeze = mUnderwaterSwitch->isUnderwater(); - mRainParticleSystem->setFrozen(freeze); -} + void SkyManager::setMoonColour (bool red) + { + if (!mCreated) return; + mSecunda->setColor(red ? mMoonScriptColor : osg::Vec4f(1,1,1,1)); + } -void SkyManager::setWeather(const WeatherResult& weather) -{ - if (!mCreated) return; - - mRainEntranceSpeed = weather.mRainEntranceSpeed; - mRainMaxRaindrops = weather.mRainMaxRaindrops; - mRainDiameter = weather.mRainDiameter; - mRainMinHeight = weather.mRainMinHeight; - mRainMaxHeight = weather.mRainMaxHeight; - mRainSpeed = weather.mRainSpeed; - mWindSpeed = weather.mWindSpeed; - mBaseWindSpeed = weather.mBaseWindSpeed; - - if (mRainEffect != weather.mRainEffect) + void SkyManager::updateRainParameters() { - mRainEffect = weather.mRainEffect; - if (!mRainEffect.empty()) + if (mRainShooter) { - createRain(); - } - else - { - destroyRain(); + float angle = -std::atan(mWindSpeed/50.f); + mRainShooter->setVelocity(osg::Vec3f(0, mRainSpeed*std::sin(angle), -mRainSpeed/std::cos(angle))); + mRainShooter->setAngle(angle); + + osg::Vec3 rainRange = osg::Vec3(mRainDiameter, mRainDiameter, (mRainMinHeight+mRainMaxHeight)/2.f); + + mPlacer->setXRange(-rainRange.x() / 2, rainRange.x() / 2); + mPlacer->setYRange(-rainRange.y() / 2, rainRange.y() / 2); + mPlacer->setZRange(-rainRange.z() / 2, rainRange.z() / 2); + + mCounter->setNumberOfParticlesPerSecondToCreate(mRainMaxRaindrops/mRainEntranceSpeed*20); } } - updateRainParameters(); + void SkyManager::switchUnderwaterRain() + { + if (!mRainParticleSystem) + return; - mIsStorm = weather.mIsStorm; + bool freeze = mUnderwaterSwitch->isUnderwater(); + mRainParticleSystem->setFrozen(freeze); + } - if (mCurrentParticleEffect != weather.mParticleEffect) + void SkyManager::setWeather(const WeatherResult& weather) { - mCurrentParticleEffect = weather.mParticleEffect; + if (!mCreated) return; - // cleanup old particles - if (mParticleEffect) - { - mParticleNode->removeChild(mParticleEffect); - mParticleEffect = nullptr; - } + mRainEntranceSpeed = weather.mRainEntranceSpeed; + mRainMaxRaindrops = weather.mRainMaxRaindrops; + mRainDiameter = weather.mRainDiameter; + mRainMinHeight = weather.mRainMinHeight; + mRainMaxHeight = weather.mRainMaxHeight; + mRainSpeed = weather.mRainSpeed; + mWindSpeed = weather.mWindSpeed; + mBaseWindSpeed = weather.mBaseWindSpeed; - if (mCurrentParticleEffect.empty()) + if (mRainEffect != weather.mRainEffect) { - if (mParticleNode) + mRainEffect = weather.mRainEffect; + if (!mRainEffect.empty()) { - mRootNode->removeChild(mParticleNode); - mParticleNode = nullptr; + createRain(); } - } - else - { - if (!mParticleNode) + else { - mParticleNode = new osg::PositionAttitudeTransform; - mParticleNode->addCullCallback(mUnderwaterSwitch); - mParticleNode->setNodeMask(Mask_WeatherParticles); - mRootNode->addChild(mParticleNode); + destroyRain(); } + } - mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); + updateRainParameters(); - SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr(new SceneUtil::FrameTimeSource)); - mParticleEffect->accept(assignVisitor); + mIsStorm = weather.mIsStorm; - AlphaFader::SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + if (mIsStorm) + mStormDirection = weather.mStormDirection; - mParticleEffect->accept(alphaFaderSetupVisitor); + if (mCurrentParticleEffect != weather.mParticleEffect) + { + mCurrentParticleEffect = weather.mParticleEffect; - SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); - mParticleEffect->accept(findPSVisitor); + // cleanup old particles + if (mParticleEffect) + { + mParticleNode->removeChild(mParticleEffect); + mParticleEffect = nullptr; + } - for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + if (mCurrentParticleEffect.empty()) { - osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); - - osg::ref_ptr program (new osgParticle::ModularProgram); - if (!mIsStorm) - program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); - program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); - program->setParticleSystem(ps); - mParticleNode->addChild(program); + if (mParticleNode) + { + mRootNode->removeChild(mParticleNode); + mParticleNode = nullptr; + } } - } - } + else + { + if (!mParticleNode) + { + mParticleNode = new osg::PositionAttitudeTransform; + mParticleNode->addCullCallback(mUnderwaterSwitch); + mParticleNode->setNodeMask(Mask_WeatherParticles); + mParticleNode->getOrCreateStateSet(); + mRootNode->addChild(mParticleNode); + } - if (mClouds != weather.mCloudTexture) - { - mClouds = weather.mCloudTexture; + mParticleEffect = mSceneManager->getInstance(mCurrentParticleEffect, mParticleNode); - std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + SceneUtil::AssignControllerSourcesVisitor assignVisitor = std::shared_ptr(new SceneUtil::FrameTimeSource); + mParticleEffect->accept(assignVisitor); - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); - cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); - cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + SetupVisitor alphaFaderSetupVisitor(mPrecipitationAlpha); + mParticleEffect->accept(alphaFaderSetupVisitor); - mCloudUpdater->setTexture(cloudTex); - } + SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); + mParticleEffect->accept(findPSVisitor); - if (mNextClouds != weather.mNextCloudTexture) - { - mNextClouds = weather.mNextCloudTexture; + for (unsigned int i = 0; i < findPSVisitor.mFoundNodes.size(); ++i) + { + osgParticle::ParticleSystem *ps = static_cast(findPSVisitor.mFoundNodes[i]); + + osg::ref_ptr program = new osgParticle::ModularProgram; + if (!mIsStorm) + program->addOperator(new WrapAroundOperator(mCamera,osg::Vec3(1024,1024,800))); + program->addOperator(new WeatherAlphaOperator(mPrecipitationAlpha, false)); + program->setParticleSystem(ps); + mParticleNode->addChild(program); + + for (int particleIndex = 0; particleIndex < ps->numParticles(); ++particleIndex) + { + ps->getParticle(particleIndex)->setAlphaRange(osgParticle::rangef(mPrecipitationAlpha, mPrecipitationAlpha)); + ps->getParticle(particleIndex)->update(0, true); + } + + ps->getOrCreateStateSet(); + ps->setUserValue("simpleLighting", true); + } + + mSceneManager->recreateShaders(mParticleNode); + } + } - if (!mNextClouds.empty()) + if (mClouds != weather.mCloudTexture) { - std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); + mClouds = weather.mCloudTexture; - osg::ref_ptr cloudTex (new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture))); + std::string texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); + + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - mCloudUpdater2->setTexture(cloudTex); + mCloudUpdater->setTexture(cloudTex); } - } - if (mCloudBlendFactor != weather.mCloudBlendFactor) - { - mCloudBlendFactor = weather.mCloudBlendFactor; + if (mStormDirection != weather.mStormDirection) + mStormDirection = weather.mStormDirection; - mCloudUpdater->setOpacity((1.f-mCloudBlendFactor)); - mCloudUpdater2->setOpacity(mCloudBlendFactor); - mCloudMesh2->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); - } + if (mNextStormDirection != weather.mNextStormDirection) + mNextStormDirection = weather.mNextStormDirection; - if (mCloudColour != weather.mFogColor) - { - osg::Vec4f clr (weather.mFogColor); - clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); + if (mNextClouds != weather.mNextCloudTexture) + { + mNextClouds = weather.mNextCloudTexture; - mCloudUpdater->setEmissionColor(clr); - mCloudUpdater2->setEmissionColor(clr); + if (!mNextClouds.empty()) + { + std::string texture = Misc::ResourceHelpers::correctTexturePath(mNextClouds, mSceneManager->getVFS()); - mCloudColour = weather.mFogColor; - } + osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); + cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); - if (mSkyColour != weather.mSkyColor) - { - mSkyColour = weather.mSkyColor; + mNextCloudUpdater->setTexture(cloudTex); + mNextStormDirection = weather.mStormDirection; + } + } - mAtmosphereUpdater->setEmissionColor(mSkyColour); - mMasser->setAtmosphereColor(mSkyColour); - mSecunda->setAtmosphereColor(mSkyColour); - } + if (mCloudBlendFactor != weather.mCloudBlendFactor) + { + mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); - if (mFogColour != weather.mFogColor) - { - mFogColour = weather.mFogColor; - } + mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); + mNextCloudUpdater->setOpacity(mCloudBlendFactor); + mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); + } + + if (mCloudColour != weather.mFogColor) + { + osg::Vec4f clr (weather.mFogColor); + clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); - mCloudSpeed = weather.mCloudSpeed; + mCloudUpdater->setEmissionColor(clr); + mNextCloudUpdater->setEmissionColor(clr); - mMasser->adjustTransparency(weather.mGlareView); - mSecunda->adjustTransparency(weather.mGlareView); + mCloudColour = weather.mFogColor; + } + + if (mSkyColour != weather.mSkyColor) + { + mSkyColour = weather.mSkyColor; + + mAtmosphereUpdater->setEmissionColor(mSkyColour); + mMasser->setAtmosphereColor(mSkyColour); + mSecunda->setAtmosphereColor(mSkyColour); + } + + if (mFogColour != weather.mFogColor) + { + mFogColour = weather.mFogColor; + } - mSun->setColor(weather.mSunDiscColor); - mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + mCloudSpeed = weather.mCloudSpeed; - float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + mMasser->adjustTransparency(weather.mGlareView); + mSecunda->adjustTransparency(weather.mGlareView); + + mSun->setColor(weather.mSunDiscColor); + mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); + + float nextStarsOpacity = weather.mNightFade * weather.mGlareView; + + if (weather.mNight && mStarsOpacity != nextStarsOpacity) + { + mStarsOpacity = nextStarsOpacity; + + mAtmosphereNightUpdater->setFade(mStarsOpacity); + } + + mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + mPrecipitationAlpha = weather.mPrecipitationAlpha; + } - if (weather.mNight && mStarsOpacity != nextStarsOpacity) + float SkyManager::getBaseWindSpeed() const { - mStarsOpacity = nextStarsOpacity; + if (!mCreated) return 0.f; - mAtmosphereNightUpdater->setFade(mStarsOpacity); + return mBaseWindSpeed; } - mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); + void SkyManager::sunEnable() + { + if (!mCreated) return; - mPrecipitationAlpha = weather.mPrecipitationAlpha; -} + mSun->setVisible(true); + } -float SkyManager::getBaseWindSpeed() const -{ - if (!mCreated) return 0.f; + void SkyManager::sunDisable() + { + if (!mCreated) return; - return mBaseWindSpeed; -} + mSun->setVisible(false); + } -void SkyManager::sunEnable() -{ - if (!mCreated) return; + void SkyManager::setStormParticleDirection(const osg::Vec3f &direction) + { + mStormParticleDirection = direction; + } - mSun->setVisible(true); -} + void SkyManager::setSunDirection(const osg::Vec3f& direction) + { + if (!mCreated) return; -void SkyManager::sunDisable() -{ - if (!mCreated) return; + mSun->setDirection(direction); + } - mSun->setVisible(false); -} + void SkyManager::setMasserState(const MoonState& state) + { + if(!mCreated) return; -void SkyManager::setStormDirection(const osg::Vec3f &direction) -{ - mStormDirection = direction; -} + mMasser->setState(state); + } -void SkyManager::setSunDirection(const osg::Vec3f& direction) -{ - if (!mCreated) return; + void SkyManager::setSecundaState(const MoonState& state) + { + if(!mCreated) return; - mSun->setDirection(direction); -} + mSecunda->setState(state); + } -void SkyManager::setMasserState(const MoonState& state) -{ - if(!mCreated) return; + void SkyManager::setDate(int day, int month) + { + mDay = day; + mMonth = month; + } - mMasser->setState(state); -} + void SkyManager::setGlareTimeOfDayFade(float val) + { + mSun->setGlareTimeOfDayFade(val); + } -void SkyManager::setSecundaState(const MoonState& state) -{ - if(!mCreated) return; + void SkyManager::setWaterHeight(float height) + { + mUnderwaterSwitch->setWaterLevel(height); + } - mSecunda->setState(state); -} + void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) + { + models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); + if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) + models.emplace_back(Settings::Manager::getString("skynight02", "Models")); + models.emplace_back(Settings::Manager::getString("skynight01", "Models")); + models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); -void SkyManager::setDate(int day, int month) -{ - mDay = day; - mMonth = month; -} + models.emplace_back(Settings::Manager::getString("weatherashcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblightcloud", "Models")); + models.emplace_back(Settings::Manager::getString("weathersnow", "Models")); + models.emplace_back(Settings::Manager::getString("weatherblizzard", "Models")); -void SkyManager::setGlareTimeOfDayFade(float val) -{ - mSun->setGlareTimeOfDayFade(val); -} + textures.emplace_back("textures/tx_mooncircle_full_s.dds"); + textures.emplace_back("textures/tx_mooncircle_full_m.dds"); -void SkyManager::setWaterHeight(float height) -{ - mUnderwaterSwitch->setWaterLevel(height); -} + textures.emplace_back("textures/tx_masser_new.dds"); + textures.emplace_back("textures/tx_masser_one_wax.dds"); + textures.emplace_back("textures/tx_masser_half_wax.dds"); + textures.emplace_back("textures/tx_masser_three_wax.dds"); + textures.emplace_back("textures/tx_masser_one_wan.dds"); + textures.emplace_back("textures/tx_masser_half_wan.dds"); + textures.emplace_back("textures/tx_masser_three_wan.dds"); + textures.emplace_back("textures/tx_masser_full.dds"); -void SkyManager::listAssetsToPreload(std::vector& models, std::vector& textures) -{ - models.emplace_back(Settings::Manager::getString("skyatmosphere", "Models")); - if (mSceneManager->getVFS()->exists(Settings::Manager::getString("skynight02", "Models"))) - models.emplace_back(Settings::Manager::getString("skynight02", "Models")); - models.emplace_back(Settings::Manager::getString("skynight01", "Models")); - models.emplace_back(Settings::Manager::getString("skyclouds", "Models")); - - models.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"); - - textures.emplace_back("textures/tx_masser_new.dds"); - textures.emplace_back("textures/tx_masser_one_wax.dds"); - textures.emplace_back("textures/tx_masser_half_wax.dds"); - textures.emplace_back("textures/tx_masser_three_wax.dds"); - textures.emplace_back("textures/tx_masser_one_wan.dds"); - textures.emplace_back("textures/tx_masser_half_wan.dds"); - textures.emplace_back("textures/tx_masser_three_wan.dds"); - textures.emplace_back("textures/tx_masser_full.dds"); - - textures.emplace_back("textures/tx_secunda_new.dds"); - textures.emplace_back("textures/tx_secunda_one_wax.dds"); - textures.emplace_back("textures/tx_secunda_half_wax.dds"); - textures.emplace_back("textures/tx_secunda_three_wax.dds"); - textures.emplace_back("textures/tx_secunda_one_wan.dds"); - textures.emplace_back("textures/tx_secunda_half_wan.dds"); - textures.emplace_back("textures/tx_secunda_three_wan.dds"); - textures.emplace_back("textures/tx_secunda_full.dds"); - - textures.emplace_back("textures/tx_sun_05.dds"); - textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); - - textures.emplace_back("textures/tx_raindrop_01.dds"); -} + textures.emplace_back("textures/tx_secunda_new.dds"); + textures.emplace_back("textures/tx_secunda_one_wax.dds"); + textures.emplace_back("textures/tx_secunda_half_wax.dds"); + textures.emplace_back("textures/tx_secunda_three_wax.dds"); + textures.emplace_back("textures/tx_secunda_one_wan.dds"); + textures.emplace_back("textures/tx_secunda_half_wan.dds"); + textures.emplace_back("textures/tx_secunda_three_wan.dds"); + textures.emplace_back("textures/tx_secunda_full.dds"); -void SkyManager::setWaterEnabled(bool enabled) -{ - mUnderwaterSwitch->setEnabled(enabled); -} + textures.emplace_back("textures/tx_sun_05.dds"); + textures.emplace_back("textures/tx_sun_flash_grey_05.dds"); + textures.emplace_back("textures/tx_raindrop_01.dds"); + } + + void SkyManager::setWaterEnabled(bool enabled) + { + mUnderwaterSwitch->setEnabled(enabled); + } } diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index f8c501dda6..1a30633886 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -8,10 +8,7 @@ #include #include -namespace osg -{ - class Camera; -} +#include "skyutil.hpp" namespace osg { @@ -19,6 +16,7 @@ namespace osg class Node; class Material; class PositionAttitudeTransform; + class Camera; } namespace osgParticle @@ -34,91 +32,6 @@ namespace Resource namespace MWRender { - class AtmosphereUpdater; - class AtmosphereNightUpdater; - class CloudUpdater; - class Sun; - class Moon; - class RainCounter; - class RainShooter; - class RainFader; - class AlphaFader; - class UnderwaterSwitchCallback; - - struct WeatherResult - { - std::string mCloudTexture; - std::string mNextCloudTexture; - float mCloudBlendFactor; - - osg::Vec4f mFogColor; - - osg::Vec4f mAmbientColor; - - osg::Vec4f mSkyColor; - - // sun light color - osg::Vec4f mSunColor; - - // alpha is the sun transparency - osg::Vec4f mSunDiscColor; - - float mFogDepth; - - float mDLFogFactor; - float mDLFogOffset; - - float mWindSpeed; - float mBaseWindSpeed; - float mCurrentWindSpeed; - float mNextWindSpeed; - - float mCloudSpeed; - - float mGlareView; - - bool mNight; // use night skybox - float mNightFade; // fading factor for night skybox - - bool mIsStorm; - - std::string mAmbientLoopSoundID; - float mAmbientSoundVolume; - - std::string mParticleEffect; - std::string mRainEffect; - float mPrecipitationAlpha; - - float mRainDiameter; - float mRainMinHeight; - float mRainMaxHeight; - float mRainSpeed; - float mRainEntranceSpeed; - int mRainMaxRaindrops; - }; - - struct MoonState - { - enum class Phase - { - Full = 0, - WaningGibbous, - ThirdQuarter, - WaningCrescent, - New, - WaxingCrescent, - FirstQuarter, - WaxingGibbous, - Unspecified - }; - - float mRotationFromHorizon; - float mRotationFromNorth; - Phase mPhase; - float mShadowBlend; - float mMoonAlpha; - }; - ///@brief The SkyManager handles rendering of the sky domes, celestial bodies as well as other objects that need to be rendered /// relative to the camera (e.g. weather particle effects) class SkyManager @@ -162,7 +75,7 @@ namespace MWRender void setRainSpeed(float speed); - void setStormDirection(const osg::Vec3f& direction); + void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); @@ -203,12 +116,12 @@ namespace MWRender osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; - osg::ref_ptr mCloudNode; + osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; - osg::ref_ptr mCloudUpdater2; - osg::ref_ptr mCloudMesh; - osg::ref_ptr mCloudMesh2; + osg::ref_ptr mNextCloudUpdater; + osg::ref_ptr mCloudMesh; + osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; @@ -239,7 +152,10 @@ namespace MWRender float mRainTimer; + // particle system rotation is independent of cloud rotation internally + osg::Vec3f mStormParticleDirection; osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; // remember some settings so we don't have to apply them again if they didn't change std::string mClouds; @@ -275,4 +191,4 @@ namespace MWRender }; } -#endif // GAME_RENDER_SKY_H +#endif diff --git a/apps/openmw/mwrender/skyutil.cpp b/apps/openmw/mwrender/skyutil.cpp new file mode 100644 index 0000000000..c0b6f81358 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.cpp @@ -0,0 +1,1140 @@ +#include "skyutil.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include + +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/weather.hpp" + +#include "vismask.hpp" +#include "renderbin.hpp" + +namespace +{ + enum class Pass + { + Atmosphere, + Atmosphere_Night, + Clouds, + Moon, + Sun, + Sunflash_Query, + Sunglare, + }; + + osg::ref_ptr createTexturedQuad(int numUvSets = 1, float scale = 1.f) + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-0.5 * scale, -0.5 * scale, 0)); + verts->push_back(osg::Vec3f(-0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, 0.5 * scale, 0)); + verts->push_back(osg::Vec3f(0.5 * scale, -0.5 * scale, 0)); + + geom->setVertexArray(verts); + + osg::ref_ptr texcoords = new osg::Vec2Array; + texcoords->push_back(osg::Vec2f(0, 1)); + texcoords->push_back(osg::Vec2f(0, 0)); + texcoords->push_back(osg::Vec2f(1, 0)); + texcoords->push_back(osg::Vec2f(1, 1)); + + osg::ref_ptr colors = new osg::Vec4Array; + colors->push_back(osg::Vec4(1.f, 1.f, 1.f, 1.f)); + geom->setColorArray(colors, osg::Array::BIND_OVERALL); + + for (int i=0; isetTexCoordArray(i, texcoords, osg::Array::BIND_PER_VERTEX); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,4)); + + return geom; + } + + struct DummyComputeBoundCallback : osg::Node::ComputeBoundingSphereCallback + { + osg::BoundingSphere computeBound(const osg::Node& node) const override + { + return osg::BoundingSphere(); + } + }; +} + +namespace MWRender +{ + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode) + { + osg::ref_ptr mat = new osg::Material; + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f)); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); + mat->setColorMode(colorMode); + return mat; + } + + osg::ref_ptr createAlphaTrackingUnlitMaterial() + { + return createUnlitMaterial(osg::Material::DIFFUSE); + } + + class SunUpdater : public SceneUtil::StateSetUpdater + { + public: + osg::Vec4f mColor; + + SunUpdater() + : mColor(1.f, 1.f, 1.f, 1.f) + { } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->setAttributeAndModes(createUnlitMaterial()); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,mColor.a())); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(mColor.r(), mColor.g(), mColor.b(), 1)); + } + }; + + OcclusionCallback::OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : mOcclusionQueryVisiblePixels(oqnVisible) + , mOcclusionQueryTotalPixels(oqnTotal) + { } + + float OcclusionCallback::getVisibleRatio (osg::Camera* camera) + { + int visible = mOcclusionQueryVisiblePixels->getQueryGeometry()->getNumPixels(camera); + int total = mOcclusionQueryTotalPixels->getQueryGeometry()->getNumPixels(camera); + + float visibleRatio = 0.f; + if (total > 0) + visibleRatio = static_cast(visible) / static_cast(total); + + float dt = MWBase::Environment::get().getFrameDuration(); + + float lastRatio = mLastRatio[osg::observer_ptr(camera)]; + + float change = dt*10; + + if (visibleRatio > lastRatio) + visibleRatio = std::min(visibleRatio, lastRatio + change); + else + visibleRatio = std::max(visibleRatio, lastRatio - change); + + mLastRatio[osg::observer_ptr(camera)] = visibleRatio; + + return visibleRatio; + } + + /// SunFlashCallback handles fading/scaling of a node depending on occlusion query result. Must be attached as a cull callback. + class SunFlashCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunFlashCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal) + : OcclusionCallback(oqnVisible, oqnTotal) + , mGlareView(1.f) + { } + + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + osg::ref_ptr stateset; + + if (visibleRatio > 0.f) + { + const float fadeThreshold = 0.1; + if (visibleRatio < fadeThreshold) + { + float fade = 1.f - (fadeThreshold - visibleRatio) / fadeThreshold; + osg::ref_ptr mat (createUnlitMaterial()); + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade*mGlareView)); + stateset = new osg::StateSet; + stateset->setAttributeAndModes(mat, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else if (visibleRatio < 1.f) + { + const float threshold = 0.6; + visibleRatio = visibleRatio * (1.f - threshold) + threshold; + } + } + + float scale = visibleRatio; + + if (scale == 0.f) + { + // no traverse + return; + } + else if (scale == 1.f) + traverse(node, cv); + else + { + osg::Matrix modelView = *cv->getModelViewMatrix(); + + modelView.preMultScale(osg::Vec3f(scale, scale, scale)); + + if (stateset) + cv->pushStateSet(stateset); + + cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF); + + traverse(node, cv); + + cv->popModelViewMatrix(); + + if (stateset) + cv->popStateSet(); + } + } + + void setGlareView(float value) + { + mGlareView = value; + } + + private: + float mGlareView; + }; + + /// SunGlareCallback controls a full-screen glare effect depending on occlusion query result and the angle between sun and camera. + /// Must be attached as a cull callback to the node above the glare node. + class SunGlareCallback : public OcclusionCallback, public SceneUtil::NodeCallback + { + public: + SunGlareCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal, + osg::ref_ptr sunTransform) + : OcclusionCallback(oqnVisible, oqnTotal) + , mSunTransform(sunTransform) + , mTimeOfDayFade(1.f) + , mGlareView(1.f) + { + mColor = Fallback::Map::getColour("Weather_Sun_Glare_Fader_Color"); + mSunGlareFaderMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Max"); + mSunGlareFaderAngleMax = Fallback::Map::getFloat("Weather_Sun_Glare_Fader_Angle_Max"); + + // Replicating a design flaw in MW. The color was being set on both ambient and emissive properties, which multiplies the result by two, + // then finally gets clamped by the fixed function pipeline. With the default INI settings, only the red component gets clamped, + // so the resulting color looks more orange than red. + mColor *= 2; + for (int i=0; i<3; ++i) + mColor[i] = std::min(1.f, mColor[i]); + } + + void operator ()(osg::Node* node, osgUtil::CullVisitor* cv) + { + float angleRadians = getAngleToSunInRadians(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + float visibleRatio = getVisibleRatio(cv->getCurrentCamera()); + + const float angleMaxRadians = osg::DegreesToRadians(mSunGlareFaderAngleMax); + + float value = 1.f - std::min(1.f, angleRadians / angleMaxRadians); + float fade = value * mSunGlareFaderMax; + + fade *= mTimeOfDayFade * mGlareView * visibleRatio; + + if (fade == 0.f) + { + // no traverse + return; + } + else + { + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr mat = createUnlitMaterial(); + + mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(0,0,0,fade)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mColor); + + stateset->setAttributeAndModes(mat); + + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } + } + + void setTimeOfDayFade(float val) + { + mTimeOfDayFade = val; + } + + void setGlareView(float glareView) + { + mGlareView = glareView; + } + + private: + float getAngleToSunInRadians(const osg::Matrix& viewMatrix) const + { + osg::Vec3d eye, center, up; + viewMatrix.getLookAt(eye, center, up); + + osg::Vec3d forward = center - eye; + osg::Vec3d sun = mSunTransform->getPosition(); + + forward.normalize(); + sun.normalize(); + float angleRadians = std::acos(forward * sun); + return angleRadians; + } + + osg::ref_ptr mSunTransform; + float mTimeOfDayFade; + float mGlareView; + osg::Vec4f mColor; + float mSunGlareFaderMax; + float mSunGlareFaderAngleMax; + }; + + struct MoonUpdater : SceneUtil::StateSetUpdater + { + Resource::ImageManager& mImageManager; + osg::ref_ptr mPhaseTex; + osg::ref_ptr mCircleTex; + float mTransparency; + float mShadowBlend; + osg::Vec4f mAtmosphereColor; + osg::Vec4f mMoonColor; + bool mForceShaders; + + MoonUpdater(Resource::ImageManager& imageManager, bool forceShaders) + : mImageManager(imageManager) + , mPhaseTex() + , mCircleTex() + , mTransparency(1.0f) + , mShadowBlend(1.0f) + , mAtmosphereColor(1.0f, 1.0f, 1.0f, 1.0f) + , mMoonColor(1.0f, 1.0f, 1.0f, 1.0f) + , mForceShaders(forceShaders) + { } + + void setDefaults(osg::StateSet* stateset) override + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Moon))); + stateset->setTextureAttributeAndModes(0, mPhaseTex); + stateset->setTextureAttributeAndModes(1, mCircleTex); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("moonBlend", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("atmosphereFade", osg::Vec4f{})); + stateset->addUniform(new osg::Uniform("diffuseMap", 0)); + stateset->addUniform(new osg::Uniform("maskMap", 1)); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + else + { + stateset->setTextureAttributeAndModes(0, mPhaseTex); + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::CONSTANT); + texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setConstantColor(osg::Vec4f(1.f, 0.f, 0.f, 1.f)); // mShadowBlend * mMoonColor + stateset->setTextureAttributeAndModes(0, texEnv); + + stateset->setTextureAttributeAndModes(1, mCircleTex); + osg::ref_ptr texEnv2 = new osg::TexEnvCombine; + texEnv2->setCombine_RGB(osg::TexEnvCombine::ADD); + texEnv2->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv2->setSource0_Alpha(osg::TexEnvCombine::TEXTURE); + texEnv2->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv2->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv2->setSource1_RGB(osg::TexEnvCombine::CONSTANT); + texEnv2->setConstantColor(osg::Vec4f(0.f, 0.f, 0.f, 1.f)); // mAtmosphereColor.rgb, mTransparency + stateset->setTextureAttributeAndModes(1, texEnv2); + stateset->setAttributeAndModes(createUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor*) override + { + if (mForceShaders) + { + stateset->setTextureAttribute(0, mPhaseTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttribute(1, mCircleTex, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + if (auto* uMoonBlend = stateset->getUniform("moonBlend")) + uMoonBlend->set(mMoonColor * mShadowBlend); + if (auto* uAtmosphereFade = stateset->getUniform("atmosphereFade")) + uAtmosphereFade->set(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mMoonColor * mShadowBlend); + + osg::TexEnvCombine* texEnv2 = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv2->setConstantColor(osg::Vec4f(mAtmosphereColor.x(), mAtmosphereColor.y(), mAtmosphereColor.z(), mTransparency)); + } + } + + void setTextures(const std::string& phaseTex, const std::string& circleTex) + { + mPhaseTex = new osg::Texture2D(mImageManager.getImage(phaseTex)); + mPhaseTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mPhaseTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mCircleTex = new osg::Texture2D(mImageManager.getImage(circleTex)); + mCircleTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mCircleTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reset(); + } + }; + + class CameraRelativeTransformCullCallback : public SceneUtil::NodeCallback + { + public: + void operator() (osg::Node* node, osgUtil::CullVisitor* cv) + { + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + unsigned int mask = 0x1; + unsigned int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, cv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; + + void AtmosphereUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void AtmosphereUpdater::setDefaults(osg::StateSet* stateset) + { + stateset->setAttributeAndModes(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere))); + } + + void AtmosphereUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + } + + AtmosphereNightUpdater::AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders) + : mColor(osg::Vec4f(0,0,0,0)) + , mTexture(new osg::Texture2D(imageManager->getWarningImage())) + , mForceShaders(forceShaders) + { } + + void AtmosphereNightUpdater::setFade(float fade) + { + mColor.a() = fade; + } + + void AtmosphereNightUpdater::setDefaults(osg::StateSet* stateset) + { + if (mForceShaders) + { + stateset->addUniform(new osg::Uniform("opacity", 0.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Atmosphere_Night))); + } + else + { + osg::ref_ptr texEnv = new osg::TexEnvCombine; + texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnv->setCombine_RGB(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + + stateset->setTextureAttributeAndModes(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureAttributeAndModes(1, texEnv, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void AtmosphereNightUpdater::apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mColor.a()); + } + else + { + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(mColor); + } + } + + CloudUpdater::CloudUpdater(bool forceShaders) + : mOpacity(0.f) + , mForceShaders(forceShaders) + { } + + void CloudUpdater::setTexture(osg::ref_ptr texture) + { + mTexture = texture; + } + + void CloudUpdater::setEmissionColor(const osg::Vec4f& emissionColor) + { + mEmissionColor = emissionColor; + } + + void CloudUpdater::setOpacity(float opacity) + { + mOpacity = opacity; + } + + void CloudUpdater::setTextureCoord(float timer) + { + mTexMat = osg::Matrixf::translate(osg::Vec3f(0.f, -timer, 0.f)); + } + + void CloudUpdater::setDefaults(osg::StateSet *stateset) + { + stateset->setAttribute(createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr texmat = new osg::TexMat; + stateset->setTextureAttributeAndModes(0, texmat); + + if (mForceShaders) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + stateset->addUniform(new osg::Uniform("opacity", 1.f)); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Clouds))); + } + else + { + stateset->setTextureAttributeAndModes(1, texmat); + // need to set opacity on a separate texture unit, diffuse alpha is used by the vertex colors already + osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; + texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnvCombine->setSource1_Alpha(osg::TexEnvCombine::CONSTANT); + texEnvCombine->setConstantColor(osg::Vec4f(1,1,1,1)); + texEnvCombine->setCombine_Alpha(osg::TexEnvCombine::MODULATE); + texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); + + stateset->setTextureAttributeAndModes(1, texEnvCombine); + + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + stateset->setTextureMode(1, GL_TEXTURE_2D, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + } + } + + void CloudUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) + { + stateset->setTextureAttribute(0, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); + mat->setEmission(osg::Material::FRONT_AND_BACK, mEmissionColor); + + osg::TexMat* texMat = static_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXMAT)); + texMat->setMatrix(mTexMat); + + if (mForceShaders) + { + stateset->getUniform("opacity")->set(mOpacity); + } + else + { + stateset->setTextureAttribute(1, mTexture, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + + osg::TexEnvCombine* texEnv = static_cast(stateset->getTextureAttribute(1, osg::StateAttribute::TEXENV)); + texEnv->setConstantColor(osg::Vec4f(1,1,1,mOpacity)); + } + } + + CameraRelativeTransform::CameraRelativeTransform() + { + // Culling works in node-local space, not in camera space, so we can't cull this node correctly + // That's not a problem though, children of this node can be culled just fine + // Just make sure you do not place a CameraRelativeTransform deep in the scene graph + setCullingActive(false); + + addCullCallback(new CameraRelativeTransformCullCallback); + } + + CameraRelativeTransform::CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) + : osg::Transform(copy, copyop) + { } + + const osg::Vec3f& CameraRelativeTransform::getLastViewPoint() const + { + return mViewPoint; + } + + bool CameraRelativeTransform::computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const + { + if (nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR) + { + mViewPoint = static_cast(nv)->getViewPoint(); + } + + if (_referenceFrame==RELATIVE_RF) + { + matrix.setTrans(osg::Vec3f(0.f,0.f,0.f)); + return false; + } + else // absolute + { + matrix.makeIdentity(); + return true; + } + } + + osg::BoundingSphere CameraRelativeTransform::computeBound() const + { + return osg::BoundingSphere(); + } + + UnderwaterSwitchCallback::UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform) + : mCameraRelativeTransform(cameraRelativeTransform) + , mEnabled(true) + , mWaterLevel(0.f) + { } + + bool UnderwaterSwitchCallback::isUnderwater() + { + osg::Vec3f viewPoint = mCameraRelativeTransform->getLastViewPoint(); + return mEnabled && viewPoint.z() < mWaterLevel; + } + + void UnderwaterSwitchCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (isUnderwater()) + return; + + traverse(node, nv); + } + + void UnderwaterSwitchCallback::setEnabled(bool enabled) + { + mEnabled = enabled; + } + void UnderwaterSwitchCallback::setWaterLevel(float waterLevel) + { + mWaterLevel = waterLevel; + } + + const float CelestialBody::mDistance = 1000.0f; + + CelestialBody::CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask) + : mVisibleMask(visibleMask) + { + mGeom = createTexturedQuad(numUvSets); + mGeom->getOrCreateStateSet(); + mTransform = new osg::PositionAttitudeTransform; + mTransform->setNodeMask(mVisibleMask); + mTransform->setScale(osg::Vec3f(450,450,450) * scaleFactor); + mTransform->addChild(mGeom); + + parentNode->addChild(mTransform); + } + + void CelestialBody::setVisible(bool visible) + { + mTransform->setNodeMask(visible ? mVisibleMask : 0); + } + + Sun::Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager) + : CelestialBody(parentNode, 1.0f, 1, Mask_Sun) + , mUpdater(new SunUpdater) + { + mTransform->addUpdateCallback(mUpdater); + + Resource::ImageManager& imageManager = *sceneManager.getImageManager(); + + osg::ref_ptr sunTex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_05.dds")); + sunTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + sunTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + sunTex->setName("diffuseMap"); + + mGeom->getOrCreateStateSet()->setTextureAttributeAndModes(0, sunTex); + mGeom->getOrCreateStateSet()->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + osg::ref_ptr queryNode = new osg::Group; + // Need to render after the world geometry so we can correctly test for occlusions + osg::StateSet* stateset = queryNode->getOrCreateStateSet(); + stateset->setRenderBinDetails(RenderBin_OcclusionQuery, "RenderBin"); + stateset->setNestRenderBins(false); + // Set up alpha testing on the occlusion testing subgraph, that way we can get the occlusion tested fragments to match the circular shape of the sun + if (!sceneManager.getForceShaders()) + { + osg::ref_ptr alphaFunc = new osg::AlphaFunc; + alphaFunc->setFunction(osg::AlphaFunc::GREATER, 0.8); + stateset->setAttributeAndModes(alphaFunc); + } + stateset->setTextureAttributeAndModes(0, sunTex); + stateset->setAttributeAndModes(createUnlitMaterial()); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunflash_Query))); + + // Disable writing to the color buffer. We are using this geometry for visibility tests only. + osg::ref_ptr colormask = new osg::ColorMask(0, 0, 0, 0); + stateset->setAttributeAndModes(colormask); + + mTransform->addChild(queryNode); + + mOcclusionQueryVisiblePixels = createOcclusionQueryNode(queryNode, true); + mOcclusionQueryTotalPixels = createOcclusionQueryNode(queryNode, false); + + createSunFlash(imageManager); + createSunGlare(); + } + + Sun::~Sun() + { + mTransform->removeUpdateCallback(mUpdater); + destroySunFlash(); + destroySunGlare(); + } + + void Sun::setColor(const osg::Vec4f& color) + { + mUpdater->mColor.r() = color.r(); + mUpdater->mColor.g() = color.g(); + mUpdater->mColor.b() = color.b(); + } + + void Sun::adjustTransparency(const float ratio) + { + mUpdater->mColor.a() = ratio; + if (mSunGlareCallback) + mSunGlareCallback->setGlareView(ratio); + if (mSunFlashCallback) + mSunFlashCallback->setGlareView(ratio); + } + + void Sun::setDirection(const osg::Vec3f& direction) + { + osg::Vec3f normalizedDirection = direction / direction.length(); + mTransform->setPosition(normalizedDirection * mDistance); + + osg::Quat quat; + quat.makeRotate(osg::Vec3f(0.0f, 0.0f, 1.0f), normalizedDirection); + mTransform->setAttitude(quat); + } + + void Sun::setGlareTimeOfDayFade(float val) + { + if (mSunGlareCallback) + mSunGlareCallback->setTimeOfDayFade(val); + } + + osg::ref_ptr Sun::createOcclusionQueryNode(osg::Group* parent, bool queryVisible) + { + osg::ref_ptr oqn = new osg::OcclusionQueryNode; + oqn->setQueriesEnabled(true); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + // With OSG 3.6.5, the method of providing user defined query geometry has been completely replaced + osg::ref_ptr queryGeom = new osg::QueryGeometry(oqn->getName()); +#else + osg::ref_ptr queryGeom = oqn->getQueryGeometry(); +#endif + + // Make it fast! A DYNAMIC query geometry means we can't break frame until the flare is rendered (which is rendered after all the other geometry, + // so that would be pretty bad). STATIC should be safe, since our node's local bounds are static, thus computeBounds() which modifies the queryGeometry + // is only called once. + // Note the debug geometry setDebugDisplay(true) is always DYNAMIC and that can't be changed, not a big deal. + queryGeom->setDataVariance(osg::Object::STATIC); + + // Set up the query geometry to match the actual sun's rendering shape. osg::OcclusionQueryNode wasn't originally intended to allow this, + // normally it would automatically adjust the query geometry to match the sub graph's bounding box. The below hack is needed to + // circumvent this. + queryGeom->setVertexArray(mGeom->getVertexArray()); + queryGeom->setTexCoordArray(0, mGeom->getTexCoordArray(0), osg::Array::BIND_PER_VERTEX); + queryGeom->removePrimitiveSet(0, queryGeom->getNumPrimitiveSets()); + queryGeom->addPrimitiveSet(mGeom->getPrimitiveSet(0)); + + // Hack to disable unwanted awful code inside OcclusionQueryNode::computeBound. + oqn->setComputeBoundingSphereCallback(new DummyComputeBoundCallback); + // Still need a proper bounding sphere. + oqn->setInitialBound(queryGeom->getBound()); + +#if OSG_VERSION_GREATER_OR_EQUAL(3, 6, 5) + oqn->setQueryGeometry(queryGeom.release()); +#endif + + osg::StateSet* queryStateSet = new osg::StateSet; + if (queryVisible) + { + osg::ref_ptr depth = new SceneUtil::AutoDepth; + // This is a trick to make fragments written by the query always use the maximum depth value, + // without having to retrieve the current far clipping distance. + // We want the sun glare to be "infinitely" far away. + double far = SceneUtil::AutoDepth::isReversed() ? 0.0 : 1.0; + depth->setFunction(osg::Depth::LEQUAL); + depth->setZNear(far); + depth->setZFar(far); + depth->setWriteMask(false); + queryStateSet->setAttributeAndModes(depth); + } + else + { + queryStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + oqn->setQueryStateSet(queryStateSet); + + parent->addChild(oqn); + + return oqn; + } + + void Sun::createSunFlash(Resource::ImageManager& imageManager) + { + osg::ref_ptr tex = new osg::Texture2D(imageManager.getImage("textures/tx_sun_flash_grey_05.dds")); + tex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + tex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + tex->setName("diffuseMap"); + + osg::ref_ptr group (new osg::Group); + + mTransform->addChild(group); + + const float scale = 2.6f; + osg::ref_ptr geom = createTexturedQuad(1, scale); + group->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, tex); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sun))); + + mSunFlashNode = group; + + mSunFlashCallback = new SunFlashCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels); + mSunFlashNode->addCullCallback(mSunFlashCallback); + } + + void Sun::destroySunFlash() + { + if (mSunFlashNode) + { + mSunFlashNode->removeCullCallback(mSunFlashCallback); + mSunFlashCallback = nullptr; + } + } + + void Sun::createSunGlare() + { + osg::ref_ptr camera = new osg::Camera; + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); // add to skyRoot instead? + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + camera->getOrCreateStateSet()->addUniform(new osg::Uniform("projectionMatrix", static_cast(camera->getProjectionMatrix()))); + SceneUtil::setCameraClearDepth(camera); + + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(2,0,0), osg::Vec3f(0,2,0)); + camera->addChild(geom); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setRenderBinDetails(RenderBin_SunGlare, "RenderBin"); + stateset->setNestRenderBins(false); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + stateset->addUniform(new osg::Uniform("pass", static_cast(Pass::Sunglare))); + + // set up additive blending + osg::ref_ptr blendFunc = new osg::BlendFunc; + blendFunc->setSource(osg::BlendFunc::SRC_ALPHA); + blendFunc->setDestination(osg::BlendFunc::ONE); + stateset->setAttributeAndModes(blendFunc); + + mSunGlareCallback = new SunGlareCallback(mOcclusionQueryVisiblePixels, mOcclusionQueryTotalPixels, mTransform); + mSunGlareNode = camera; + + mSunGlareNode->addCullCallback(mSunGlareCallback); + + mTransform->addChild(camera); + } + + void Sun::destroySunGlare() + { + if (mSunGlareNode) + { + mSunGlareNode->removeCullCallback(mSunGlareCallback); + mSunGlareCallback = nullptr; + } + } + + Moon::Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type) + : CelestialBody(parentNode, scaleFactor, 2) + , mType(type) + , mPhase(MoonState::Phase::Unspecified) + , mUpdater(new MoonUpdater(*sceneManager.getImageManager(), sceneManager.getForceShaders())) + { + setPhase(MoonState::Phase::Full); + setVisible(true); + + mGeom->addUpdateCallback(mUpdater); + } + + Moon::~Moon() + { + mGeom->removeUpdateCallback(mUpdater); + } + + void Moon::adjustTransparency(const float ratio) + { + mUpdater->mTransparency *= ratio; + } + + void Moon::setState(const MoonState state) + { + float radsX = ((state.mRotationFromHorizon) * static_cast(osg::PI)) / 180.0f; + float radsZ = ((state.mRotationFromNorth) * static_cast(osg::PI)) / 180.0f; + + osg::Quat rotX(radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + osg::Quat rotZ(radsZ, osg::Vec3f(0.0f, 0.0f, 1.0f)); + + osg::Vec3f direction = rotX * rotZ * osg::Vec3f(0.0f, 1.0f, 0.0f); + mTransform->setPosition(direction * mDistance); + + // The moon quad is initially oriented facing down, so we need to offset its X-axis + // rotation to rotate it to face the camera when sitting at the horizon. + osg::Quat attX((-static_cast(osg::PI) / 2.0f) + radsX, osg::Vec3f(1.0f, 0.0f, 0.0f)); + mTransform->setAttitude(attX * rotZ); + + setPhase(state.mPhase); + mUpdater->mTransparency = state.mMoonAlpha; + mUpdater->mShadowBlend = state.mShadowBlend; + } + + void Moon::setAtmosphereColor(const osg::Vec4f& color) + { + mUpdater->mAtmosphereColor = color; + } + + void Moon::setColor(const osg::Vec4f& color) + { + mUpdater->mMoonColor = color; + } + + unsigned int Moon::getPhaseInt() const + { + switch (mPhase) + { + case MoonState::Phase::New: + return 0; + case MoonState::Phase::WaxingCrescent: + return 1; + case MoonState::Phase::WaningCrescent: + return 1; + case MoonState::Phase::FirstQuarter: + return 2; + case MoonState::Phase::ThirdQuarter: + return 2; + case MoonState::Phase::WaxingGibbous: + return 3; + case MoonState::Phase::WaningGibbous: + return 3; + case MoonState::Phase::Full: + return 4; + default: + return 0; + } + } + + void Moon::setPhase(const MoonState::Phase& phase) + { + if(mPhase == phase) + return; + + mPhase = phase; + + std::string textureName = "textures/tx_"; + + if (mType == Moon::Type_Secunda) + textureName += "secunda_"; + else + textureName += "masser_"; + + switch (mPhase) + { + case MoonState::Phase::New: + textureName += "new"; + break; + case MoonState::Phase::WaxingCrescent: + textureName += "one_wax"; + break; + case MoonState::Phase::FirstQuarter: + textureName += "half_wax"; + break; + case MoonState::Phase::WaxingGibbous: + textureName += "three_wax"; + break; + case MoonState::Phase::WaningCrescent: + textureName += "one_wan"; + break; + case MoonState::Phase::ThirdQuarter: + textureName += "half_wan"; + break; + case MoonState::Phase::WaningGibbous: + textureName += "three_wan"; + break; + case MoonState::Phase::Full: + textureName += "full"; + break; + default: + break; + } + + textureName += ".dds"; + + if (mType == Moon::Type_Secunda) + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_s.dds"); + else + mUpdater->setTextures(textureName, "textures/tx_mooncircle_full_m.dds"); + } + + int RainCounter::numParticlesToCreate(double dt) const + { + // limit dt to avoid large particle emissions if there are jumps in the simulation time + // 0.2 seconds is the same cap as used in Engine's frame loop + dt = std::min(dt, 0.2); + return ConstantRateCounter::numParticlesToCreate(dt); + } + + RainShooter::RainShooter() + : mAngle(0.f) + { } + + void RainShooter::shoot(osgParticle::Particle* particle) const + { + particle->setVelocity(mVelocity); + particle->setAngle(osg::Vec3f(-mAngle, 0, (Misc::Rng::rollProbability() * 2 - 1) * osg::PI)); + } + + void RainShooter::setVelocity(const osg::Vec3f& velocity) + { + mVelocity = velocity; + } + + void RainShooter::setAngle(float angle) + { + mAngle = angle; + } + + osg::Object* RainShooter::cloneType() const + { + return new RainShooter; + } + + osg::Object* RainShooter::clone(const osg::CopyOp &) const + { + return new RainShooter(*this); + } + + ModVertexAlphaVisitor::ModVertexAlphaVisitor(ModVertexAlphaVisitor::MeshType type) + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + , mType(type) + { } + + void ModVertexAlphaVisitor::apply(osg::Geometry& geometry) + { + osg::ref_ptr colors = new osg::Vec4Array(geometry.getVertexArray()->getNumElements()); + for (unsigned int i=0; isize(); ++i) + { + float alpha = 1.f; + + switch (mType) + { + case ModVertexAlphaVisitor::Atmosphere: + { + // this is a cylinder, so every second vertex belongs to the bottom-most row + alpha = (i%2) ? 0.f : 1.f; + break; + } + case ModVertexAlphaVisitor::Clouds: + { + if (i>= 49 && i <= 64) + alpha = 0.f; // bottom-most row + else if (i>= 33 && i <= 48) + alpha = 0.25098; // second row + else + alpha = 1.f; + break; + } + case ModVertexAlphaVisitor::Stars: + { + if (geometry.getColorArray()) + { + osg::Vec4Array* origColors = static_cast(geometry.getColorArray()); + alpha = ((*origColors)[i].x() == 1.f) ? 1.f : 0.f; + } + else + alpha = 1.f; + break; + } + } + + (*colors)[i] = osg::Vec4f(0.f, 0.f, 0.f, alpha); + } + + geometry.setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } +} diff --git a/apps/openmw/mwrender/skyutil.hpp b/apps/openmw/mwrender/skyutil.hpp new file mode 100644 index 0000000000..a0d9ed72b4 --- /dev/null +++ b/apps/openmw/mwrender/skyutil.hpp @@ -0,0 +1,343 @@ +#ifndef OPENMW_MWRENDER_SKYUTIL_H +#define OPENMW_MWRENDER_SKYUTIL_H + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Resource +{ + class ImageManager; + class SceneManager; +} + +namespace MWRender +{ + struct MoonUpdater; + class SunUpdater; + class SunFlashCallback; + class SunGlareCallback; + + struct WeatherResult + { + std::string mCloudTexture; + std::string mNextCloudTexture; + float mCloudBlendFactor; + + osg::Vec4f mFogColor; + + osg::Vec4f mAmbientColor; + + osg::Vec4f mSkyColor; + + // sun light color + osg::Vec4f mSunColor; + + // alpha is the sun transparency + osg::Vec4f mSunDiscColor; + + float mFogDepth; + + float mDLFogFactor; + float mDLFogOffset; + + float mWindSpeed; + float mBaseWindSpeed; + float mCurrentWindSpeed; + float mNextWindSpeed; + + float mCloudSpeed; + + float mGlareView; + + bool mNight; // use night skybox + float mNightFade; // fading factor for night skybox + + bool mIsStorm; + + std::string mAmbientLoopSoundID; + float mAmbientSoundVolume; + + std::string mParticleEffect; + std::string mRainEffect; + float mPrecipitationAlpha; + + float mRainDiameter; + float mRainMinHeight; + float mRainMaxHeight; + float mRainSpeed; + float mRainEntranceSpeed; + int mRainMaxRaindrops; + + osg::Vec3f mStormDirection; + osg::Vec3f mNextStormDirection; + }; + + struct MoonState + { + enum class Phase + { + Full, + WaningGibbous, + ThirdQuarter, + WaningCrescent, + New, + WaxingCrescent, + FirstQuarter, + WaxingGibbous, + Unspecified + }; + + float mRotationFromHorizon; + float mRotationFromNorth; + Phase mPhase; + float mShadowBlend; + float mMoonAlpha; + }; + + osg::ref_ptr createAlphaTrackingUnlitMaterial(); + osg::ref_ptr createUnlitMaterial(osg::Material::ColorMode colorMode = osg::Material::OFF); + + class OcclusionCallback + { + public: + OcclusionCallback(osg::ref_ptr oqnVisible, osg::ref_ptr oqnTotal); + + protected: + float getVisibleRatio (osg::Camera* camera); + + private: + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + + std::map, float> mLastRatio; + }; + + class AtmosphereUpdater : public SceneUtil::StateSetUpdater + { + public: + void setEmissionColor(const osg::Vec4f& emissionColor); + + protected: + void setDefaults(osg::StateSet* stateset) override; + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mEmissionColor; + }; + + class AtmosphereNightUpdater : public SceneUtil::StateSetUpdater + { + public: + AtmosphereNightUpdater(Resource::ImageManager* imageManager, bool forceShaders); + + void setFade(float fade); + + protected: + void setDefaults(osg::StateSet* stateset) override; + + void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override; + + private: + osg::Vec4f mColor; + osg::ref_ptr mTexture; + bool mForceShaders; + }; + + class CloudUpdater : public SceneUtil::StateSetUpdater + { + public: + CloudUpdater(bool forceShaders); + + void setTexture(osg::ref_ptr texture); + + void setEmissionColor(const osg::Vec4f& emissionColor); + void setOpacity(float opacity); + void setTextureCoord(float timer); + + protected: + void setDefaults(osg::StateSet *stateset) override; + void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; + + private: + osg::ref_ptr mTexture; + osg::Vec4f mEmissionColor; + float mOpacity; + bool mForceShaders; + osg::Matrixf mTexMat; + }; + + /// Transform that removes the eyepoint of the modelview matrix, + /// i.e. its children are positioned relative to the camera. + class CameraRelativeTransform : public osg::Transform + { + public: + CameraRelativeTransform(); + + CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop); + + META_Node(MWRender, CameraRelativeTransform) + + const osg::Vec3f& getLastViewPoint() const; + + bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const override; + + osg::BoundingSphere computeBound() const override; + + private: + // viewPoint for the current frame + mutable osg::Vec3f mViewPoint; + }; + + /// @brief Hides the node subgraph if the eye point is below water. + /// @note Must be added as cull callback. + /// @note Meant to be used on a node that is child of a CameraRelativeTransform. + /// The current view point must be retrieved by the CameraRelativeTransform since we can't get it anymore once we are in camera-relative space. + class UnderwaterSwitchCallback : public SceneUtil::NodeCallback + { + public: + UnderwaterSwitchCallback(CameraRelativeTransform* cameraRelativeTransform); + bool isUnderwater(); + + void operator()(osg::Node* node, osg::NodeVisitor* nv); + void setEnabled(bool enabled); + void setWaterLevel(float waterLevel); + + private: + osg::ref_ptr mCameraRelativeTransform; + bool mEnabled; + float mWaterLevel; + }; + + /// A base class for the sun and moons. + class CelestialBody + { + public: + CelestialBody(osg::Group* parentNode, float scaleFactor, int numUvSets, unsigned int visibleMask=~0u); + + virtual ~CelestialBody() = default; + + virtual void adjustTransparency(const float ratio) = 0; + + void setVisible(bool visible); + + protected: + unsigned int mVisibleMask; + static const float mDistance; + osg::ref_ptr mTransform; + osg::ref_ptr mGeom; + }; + + class Sun : public CelestialBody + { + public: + Sun(osg::Group* parentNode, Resource::SceneManager& sceneManager); + + ~Sun(); + + void setColor(const osg::Vec4f& color); + void adjustTransparency(const float ratio) override; + + void setDirection(const osg::Vec3f& direction); + void setGlareTimeOfDayFade(float val); + + private: + /// @param queryVisible If true, queries the amount of visible pixels. If false, queries the total amount of pixels. + osg::ref_ptr createOcclusionQueryNode(osg::Group* parent, bool queryVisible); + + void createSunFlash(Resource::ImageManager& imageManager); + void destroySunFlash(); + + void createSunGlare(); + void destroySunGlare(); + + osg::ref_ptr mUpdater; + osg::ref_ptr mSunFlashNode; + osg::ref_ptr mSunGlareNode; + osg::ref_ptr mSunFlashCallback; + osg::ref_ptr mSunGlareCallback; + osg::ref_ptr mOcclusionQueryVisiblePixels; + osg::ref_ptr mOcclusionQueryTotalPixels; + }; + + class Moon : public CelestialBody + { + public: + enum Type + { + Type_Masser = 0, + Type_Secunda + }; + + Moon(osg::Group* parentNode, Resource::SceneManager& sceneManager, float scaleFactor, Type type); + + ~Moon(); + + void adjustTransparency(const float ratio) override; + void setState(const MoonState state); + void setAtmosphereColor(const osg::Vec4f& color); + void setColor(const osg::Vec4f& color); + + unsigned int getPhaseInt() const; + + private: + Type mType; + MoonState::Phase mPhase; + osg::ref_ptr mUpdater; + + void setPhase(const MoonState::Phase& phase); + }; + + class RainCounter : public osgParticle::ConstantRateCounter + { + public: + int numParticlesToCreate(double dt) const override; + }; + + class RainShooter : public osgParticle::Shooter + { + public: + RainShooter(); + + osg::Object* cloneType() const override; + + osg::Object* clone(const osg::CopyOp &) const override; + + void shoot(osgParticle::Particle* particle) const override; + + void setVelocity(const osg::Vec3f& velocity); + void setAngle(float angle); + + private: + osg::Vec3f mVelocity; + float mAngle; + }; + + class ModVertexAlphaVisitor : public osg::NodeVisitor + { + public: + enum MeshType + { + Atmosphere, + Stars, + Clouds + }; + + ModVertexAlphaVisitor(MeshType type); + + void apply(osg::Geometry& geometry) override; + + private: + MeshType mType; + }; +} + +#endif diff --git a/apps/openmw/mwrender/viewovershoulder.cpp b/apps/openmw/mwrender/viewovershoulder.cpp deleted file mode 100644 index 799e34c992..0000000000 --- a/apps/openmw/mwrender/viewovershoulder.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "viewovershoulder.hpp" - -#include - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/ptr.hpp" -#include "../mwworld/refdata.hpp" - -#include "../mwmechanics/drawstate.hpp" - -namespace MWRender -{ - - ViewOverShoulderController::ViewOverShoulderController(Camera* camera) : - mCamera(camera), mMode(Mode::RightShoulder), - mAutoSwitchShoulder(Settings::Manager::getBool("auto switch shoulder", "Camera")), - mOverShoulderHorizontalOffset(30.f), mOverShoulderVerticalOffset(-10.f) - { - osg::Vec2f offset = Settings::Manager::getVector2("view over shoulder offset", "Camera"); - mOverShoulderHorizontalOffset = std::abs(offset.x()); - mOverShoulderVerticalOffset = offset.y(); - mDefaultShoulderIsRight = offset.x() >= 0; - - mCamera->enableDynamicCameraDistance(true); - mCamera->enableCrosshairInThirdPersonMode(true); - mCamera->setFocalPointTargetOffset(offset); - } - - void ViewOverShoulderController::update() - { - if (mCamera->isFirstPerson()) - return; - - Mode oldMode = mMode; - auto ptr = mCamera->getTrackingPtr(); - bool combat = ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Nothing; - if (combat && !mCamera->isVanityOrPreviewModeEnabled()) - mMode = Mode::Combat; - else if (MWBase::Environment::get().getWorld()->isSwimming(ptr)) - mMode = Mode::Swimming; - else if (oldMode == Mode::Combat || oldMode == Mode::Swimming) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - if (mAutoSwitchShoulder && (mMode == Mode::LeftShoulder || mMode == Mode::RightShoulder)) - trySwitchShoulder(); - - if (oldMode == mMode) - return; - - if (mCamera->getMode() == Camera::Mode::Vanity) - // Player doesn't touch controls for a long time. Transition should be very slow. - mCamera->setFocalPointTransitionSpeed(0.2f); - else if ((oldMode == Mode::Combat || mMode == Mode::Combat) && mCamera->getMode() == Camera::Mode::Normal) - // Transition to/from combat mode and we are not it preview mode. Should be fast. - mCamera->setFocalPointTransitionSpeed(5.f); - else - mCamera->setFocalPointTransitionSpeed(1.f); // Default transition speed. - - switch (mMode) - { - case Mode::RightShoulder: - mCamera->setFocalPointTargetOffset({mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::LeftShoulder: - mCamera->setFocalPointTargetOffset({-mOverShoulderHorizontalOffset, mOverShoulderVerticalOffset}); - break; - case Mode::Combat: - case Mode::Swimming: - default: - mCamera->setFocalPointTargetOffset({0, 15}); - } - } - - void ViewOverShoulderController::trySwitchShoulder() - { - if (mCamera->getMode() != Camera::Mode::Normal) - return; - - const float limitToSwitch = 120; // switch to other shoulder if wall is closer than this limit - const float limitToSwitchBack = 300; // switch back to default shoulder if there is no walls at this distance - - auto orient = osg::Quat(mCamera->getYaw(), osg::Vec3d(0,0,1)); - osg::Vec3d playerPos = mCamera->getFocalPoint() - mCamera->getFocalPointOffset(); - - MWBase::World* world = MWBase::Environment::get().getWorld(); - osg::Vec3d sideOffset = orient * osg::Vec3d(world->getHalfExtents(mCamera->getTrackingPtr()).x() - 1, 0, 0); - float rayRight = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 0, 0), limitToSwitchBack + 1); - float rayLeft = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 0, 0), limitToSwitchBack + 1); - float rayRightForward = world->getDistToNearestRayHit( - playerPos + sideOffset, orient * osg::Vec3d(1, 3, 0), limitToSwitchBack + 1); - float rayLeftForward = world->getDistToNearestRayHit( - playerPos - sideOffset, orient * osg::Vec3d(-1, 3, 0), limitToSwitchBack + 1); - float distRight = std::min(rayRight, rayRightForward); - float distLeft = std::min(rayLeft, rayLeftForward); - - if (distLeft < limitToSwitch && distRight > limitToSwitchBack) - mMode = Mode::RightShoulder; - else if (distRight < limitToSwitch && distLeft > limitToSwitchBack) - mMode = Mode::LeftShoulder; - else if (distRight > limitToSwitchBack && distLeft > limitToSwitchBack) - mMode = mDefaultShoulderIsRight ? Mode::RightShoulder : Mode::LeftShoulder; - } - -} diff --git a/apps/openmw/mwrender/viewovershoulder.hpp b/apps/openmw/mwrender/viewovershoulder.hpp deleted file mode 100644 index 80ac308656..0000000000 --- a/apps/openmw/mwrender/viewovershoulder.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef VIEWOVERSHOULDER_H -#define VIEWOVERSHOULDER_H - -#include "camera.hpp" - -namespace MWRender -{ - - class ViewOverShoulderController - { - public: - ViewOverShoulderController(Camera* camera); - - void update(); - - private: - void trySwitchShoulder(); - enum class Mode { RightShoulder, LeftShoulder, Combat, Swimming }; - - Camera* mCamera; - Mode mMode; - bool mAutoSwitchShoulder; - float mOverShoulderHorizontalOffset; - float mOverShoulderVerticalOffset; - bool mDefaultShoulderIsRight; - }; - -} - -#endif // VIEWOVERSHOULDER_H diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 87ca9415fa..a7a28614cb 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -58,6 +58,9 @@ namespace MWRender Mask_Groundcover = (1<<20), }; + // Defines masks to remove when using ToggleWorld command + constexpr static unsigned int sToggleWorldMask = Mask_Debug | Mask_Actor | Mask_Terrain | Mask_Object | Mask_Static | Mask_Groundcover; + } #endif diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 37368b8e7a..fd5cbe0f79 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -27,11 +27,12 @@ #include #include -#include +#include #include #include #include +#include #include @@ -260,21 +261,19 @@ class Refraction : public SceneUtil::RTTNode public: Refraction(uint32_t rttSize) : RTTNode(rttSize, rttSize, 1, false) + , mNodeMask(Refraction::sDefaultCullMask) { mClipCullNode = new ClipCullNode; } void setDefaults(osg::Camera* camera) override { - SceneUtil::setCameraClearDepth(camera); camera->setReferenceFrame(osg::Camera::RELATIVE_RF); camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); camera->setName("RefractionCamera"); camera->addCullCallback(new InheritViewPointCallback); camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - camera->setCullMask(Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover); - // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) @@ -293,6 +292,7 @@ public: void apply(osg::Camera* camera) override { camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setScene(osg::Node* scene) @@ -305,8 +305,7 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f, std::max(0.0f, - Settings::Manager::getFloat("refraction scale", "Water"))); + const float refractionScale = std::clamp(Settings::Manager::getFloat("refraction scale", "Water"), 0.f, 1.f); mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); @@ -314,10 +313,22 @@ public: mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } + void showWorld(bool show) + { + if (show) + mNodeMask = Refraction::sDefaultCullMask; + else + mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; + } + private: osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + + unsigned int mNodeMask; + + static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover; }; class Reflection : public SceneUtil::RTTNode @@ -332,7 +343,6 @@ public: void setDefaults(osg::Camera* camera) override { - SceneUtil::setCameraClearDepth(camera); camera->setReferenceFrame(osg::Camera::RELATIVE_RF); camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); camera->setName("ReflectionCamera"); @@ -357,15 +367,8 @@ public: void setInterior(bool isInterior) { - int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); - reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail)); - unsigned int extraMask = 0; - if(reflectionDetail >= 1) extraMask |= Mask_Terrain; - if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; - if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - mNodeMask = Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + mInterior = isInterior; + mNodeMask = calcNodeMask(); } void setWaterLevel(float waterLevel) @@ -382,11 +385,34 @@ public: mClipCullNode->addChild(scene); } + void showWorld(bool show) + { + if (show) + mNodeMask = calcNodeMask(); + else + mNodeMask = calcNodeMask() & ~sToggleWorldMask; + } + private: + + unsigned int calcNodeMask() + { + int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water"); + reflectionDetail = std::clamp(reflectionDetail, mInterior ? 2 : 0, 5); + unsigned int extraMask = 0; + if(reflectionDetail >= 1) extraMask |= Mask_Terrain; + if(reflectionDetail >= 2) extraMask |= Mask_Static; + if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; + if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; + return Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; + } + osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Node::NodeMask mNodeMask; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; + bool mInterior; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -422,6 +448,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mToggled(true) , mTop(0) , mInterior(false) + , mShowWorld(true) , mCullCallback(nullptr) , mShaderWaterStateSetUpdater(nullptr) { @@ -519,6 +546,8 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } + showWorld(mShowWorld); + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else @@ -552,7 +581,7 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) // Add animated textures std::vector > textures; - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; isetMode(GL_BLEND, osg::StateAttribute::ON); stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); - osg::ref_ptr depth = SceneUtil::createDepth(); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); } @@ -646,7 +675,10 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R { // use a define map to conditionally compile the shader std::map defineMap; - defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(mRefraction ? "1" : "0"))); + defineMap["refraction_enabled"] = std::string(mRefraction ? "1" : "0"); + const auto rippleDetail = std::clamp(Settings::Manager::getInt("rain ripple detail", "Water"), 0, 2); + defineMap["rain_ripple_detail"] = std::to_string(rippleDetail); + Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr vertexShader(shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); @@ -693,7 +725,7 @@ Water::~Water() void Water::listAssetsToPreload(std::vector &textures) { - int frameCount = std::max(0, std::min(Fallback::Map::getInt("Water_SurfaceFrameCount"), 320)); + const int frameCount = std::clamp(Fallback::Map::getInt("Water_SurfaceFrameCount"), 0, 320); const std::string& texture = Fallback::Map::getString("Water_SurfaceTexture"); for (int i=0; iclear(); } +void Water::showWorld(bool show) +{ + if (mReflection) + mReflection->showWorld(show); + if (mRefraction) + mRefraction->showWorld(show); + mShowWorld = show; +} + } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 719e4fdc2b..c7acbf708f 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -71,6 +71,7 @@ namespace MWRender bool mToggled; float mTop; bool mInterior; + bool mShowWorld; osg::Callback* mCullCallback; osg::ref_ptr mShaderWaterStateSetUpdater; @@ -124,6 +125,8 @@ namespace MWRender osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); + + void showWorld(bool show); }; } diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index b89cfd8df1..0572eae3be 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -66,7 +66,7 @@ void WeaponAnimation::attachArrow(const MWWorld::Ptr& actor) MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weaponSlot == inv.end()) return; - if (weaponSlot->getTypeName() != typeid(ESM::Weapon).name()) + if (weaponSlot->getType() != ESM::Weapon::sRecordId) return; int type = weaponSlot->get()->mBase->mData.mType; @@ -109,7 +109,7 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end()) return; - if (weapon->getTypeName() != typeid(ESM::Weapon).name()) + if (weapon->getType() != ESM::Weapon::sRecordId) return; // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. @@ -172,14 +172,13 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) } } -void WeaponAnimation::addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) +void WeaponAnimation::addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>> &map, osg::Node* objectRoot) { for (int i=0; i<2; ++i) { mSpineControllers[i] = nullptr; - std::map >::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); + Animation::NodeMap::const_iterator found = nodes.find(i == 0 ? "bip01 spine1" : "bip01 spine2"); if (found != nodes.end()) { osg::Node* node = found->second; diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index 1f614463a6..125587c1bd 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -42,8 +42,7 @@ namespace MWRender void releaseArrow(MWWorld::Ptr actor, float attackStrength); /// Add WeaponAnimation-related controllers to \a nodes and store the added controllers in \a map. - void addControllers(const std::map >& nodes, - std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); + void addControllers(const Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index c5a4bb6dfc..b7b6de9463 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -14,6 +14,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/aiactivate.hpp" #include "../mwmechanics/aiescort.hpp" @@ -47,10 +48,14 @@ namespace MWScript std::string objectID = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; i(duration), x, y, z); + if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) + return; + + MWMechanics::AiEscort escortPackage(actorID, static_cast(duration), x, y, z, repeat); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; @@ -145,15 +158,20 @@ namespace MWScript Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; igetStore().get().find(cellID); + if (!MWBase::Environment::get().getWorld()->getStore().get().search(cellID)) + return; - MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z); + MWMechanics::AiEscort escortPackage(actorID, cellID, static_cast(duration), x, y, z, repeat); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; @@ -169,9 +187,11 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - Interpreter::Type_Integer value = ptr.getClass().getCreatureStats (ptr).getAiSequence().isPackageDone(); + bool done = false; + if (ptr.getClass().isActor()) + done = ptr.getClass().getCreatureStats(ptr).getAiSequence().isPackageDone(); - runtime.push (value); + runtime.push(done); } }; @@ -208,8 +228,7 @@ namespace MWScript { if(!repeat) repeat = true; - Interpreter::Type_Integer idleValue = runtime[0].mInteger; - idleValue = std::min(255, std::max(0, idleValue)); + Interpreter::Type_Integer idleValue = std::clamp(runtime[0].mInteger, 0, 255); idleList.push_back(idleValue); runtime.pop(); --arg0; @@ -222,9 +241,12 @@ namespace MWScript --arg0; } - // discard additional arguments (reset), because we have no idea what they mean. + // discard additional arguments, because we have no idea what they mean. for (unsigned int i=0; i @@ -257,6 +282,9 @@ namespace MWScript Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); + if (!ptr.getClass().isActor()) + return; + int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value; ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified); @@ -307,10 +335,14 @@ namespace MWScript Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); - // discard additional arguments (reset), because we have no idea what they mean. + // The value of the reset argument doesn't actually matter + bool repeat = arg0; for (unsigned int i=0; igetGreetingState(actor) == MWMechanics::Greet_InProgress; - bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); - targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); + const MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); + MWWorld::Ptr targetPtr; + if (creatureStats.getAiSequence().getCombatTarget(targetPtr)) + { + if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId) + targetsAreEqual = true; + } + else if (testedTargetId == "player") // Currently the player ID is hardcoded + { + MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); + bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; + bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); + targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); + } } - runtime.push(int(targetsAreEqual)); + runtime.push(targetsAreEqual); } }; @@ -475,8 +513,9 @@ namespace MWScript void execute (Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = R()(runtime); - MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); - creatureStats.getAiSequence().stopCombat(); + if (!actor.getClass().isActor()) + return; + MWBase::Environment::get().getMechanicsManager()->stopCombat(actor); } }; @@ -506,6 +545,9 @@ namespace MWScript Interpreter::Type_Float y = runtime[0].mFloat; runtime.pop(); + if (!actor.getClass().isActor() || actor == MWMechanics::getPlayer()) + return; + MWMechanics::AiFace facePackage(x, y); actor.getClass().getCreatureStats(actor).getAiSequence().stack(facePackage, actor); } diff --git a/apps/openmw/mwscript/compilercontext.cpp b/apps/openmw/mwscript/compilercontext.cpp index 4a7038e1cb..983365e06a 100644 --- a/apps/openmw/mwscript/compilercontext.cpp +++ b/apps/openmw/mwscript/compilercontext.cpp @@ -86,14 +86,4 @@ namespace MWScript store.get().search (name) || store.get().search (name); } - - bool CompilerContext::isJournalId (const std::string& name) const - { - const MWWorld::ESMStore &store = - MWBase::Environment::get().getWorld()->getStore(); - - const ESM::Dialogue *topic = store.get().search (name); - - return topic && topic->mType==ESM::Dialogue::Journal; - } } diff --git a/apps/openmw/mwscript/compilercontext.hpp b/apps/openmw/mwscript/compilercontext.hpp index 00b10ea06d..d800781fd8 100644 --- a/apps/openmw/mwscript/compilercontext.hpp +++ b/apps/openmw/mwscript/compilercontext.hpp @@ -39,9 +39,6 @@ namespace MWScript bool isId (const std::string& name) const override; ///< Does \a name match an ID, that can be referenced? - - bool isJournalId (const std::string& name) const override; - ///< Does \a name match a journal ID? }; } diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 7eeb3bfaba..501404e958 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -50,7 +50,7 @@ namespace void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::Ptr& owner, MWWorld::ContainerStore& store, bool topLevel = true) { - if(itemPtr.getTypeName() == typeid(ESM::ItemLevList).name()) + if(itemPtr.getType() == ESM::ItemLevList::sRecordId) { const ESM::ItemLevList* levItemList = itemPtr.get()->mBase; @@ -108,7 +108,7 @@ namespace MWScript // Check if "item" can be placed in a container MWWorld::ManualRef manualRef(MWBase::Environment::get().getWorld()->getStore(), item, 1); MWWorld::Ptr itemPtr = manualRef.getPtr(); - bool isLevelledList = itemPtr.getClass().getTypeName() == typeid(ESM::ItemLevList).name(); + bool isLevelledList = itemPtr.getClass().getType() == ESM::ItemLevList::sRecordId; if(!isLevelledList) MWWorld::ContainerStore::getType(itemPtr); @@ -120,7 +120,7 @@ namespace MWScript } // Calls to unresolved containers affect the base record - if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && (!ptr.getRefData().getCustomData() || + if(ptr.getClass().getType() == ESM::Container::sRecordId && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, count); @@ -232,7 +232,7 @@ namespace MWScript return; } // Calls to unresolved containers affect the base record instead - else if(ptr.getClass().getTypeName() == typeid(ESM::Container).name() && + else if(ptr.getClass().getType() == ESM::Container::sRecordId && (!ptr.getRefData().getCustomData() || !ptr.getClass().getContainerStore(ptr).isResolved())) { ptr.getClass().modifyBaseInventory(ptr.getCellRef().getRefId(), item, -count); @@ -380,7 +380,7 @@ namespace MWScript const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore (ptr); MWWorld::ConstContainerStoreIterator it = invStore.getSlot (slot); - if (it == invStore.end() || it->getTypeName () != typeid(ESM::Armor).name()) + if (it == invStore.end() || it->getType () != ESM::Armor::sRecordId) { runtime.push(-1); return; @@ -464,13 +464,13 @@ namespace MWScript runtime.push(-1); return; } - else if (it->getTypeName() != typeid(ESM::Weapon).name()) + else if (it->getType() != ESM::Weapon::sRecordId) { - if (it->getTypeName() == typeid(ESM::Lockpick).name()) + if (it->getType() == ESM::Lockpick::sRecordId) { runtime.push(-2); } - else if (it->getTypeName() == typeid(ESM::Probe).name()) + else if (it->getType() == ESM::Probe::sRecordId) { runtime.push(-3); } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 3642f68dc2..5d9f037357 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -310,7 +310,7 @@ namespace MWScript // Instantly reset door to closed state // This is done when using Lock in scripts, but not when using Lock spells. - if (ptr.getTypeName() == typeid(ESM::Door).name() && !ptr.getCellRef().getTeleport()) + if (ptr.getType() == ESM::Door::sRecordId && !ptr.getCellRef().getTeleport()) { MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index 2b6bf826f9..81984ad7b7 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -11,6 +11,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" + #include "interpretercontext.hpp" namespace MWScript @@ -91,7 +93,11 @@ namespace MWScript Interpreter::Type_Integer id = runtime[0].mInteger; runtime.pop(); - MWBase::Environment::get().getWorld()->changeWeather(region, id); + const ESM::Region* reg = MWBase::Environment::get().getWorld()->getStore().get().search(region); + if (reg) + MWBase::Environment::get().getWorld()->changeWeather(region, id); + else + runtime.getContext().report("Warning: Region \"" + region + "\" was not found"); } }; @@ -108,7 +114,7 @@ namespace MWScript chances.reserve(10); while(arg0 > 0) { - chances.push_back(std::max(0, std::min(127, runtime[0].mInteger))); + chances.push_back(std::clamp(runtime[0].mInteger, 0, 127)); runtime.pop(); arg0--; } diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index ccad186a0d..d7120af53a 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -1194,13 +1194,23 @@ namespace MWScript { bool wasEnabled = ptr.getRefData().isEnabled(); MWBase::Environment::get().getWorld()->undeleteObject(ptr); - MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); - MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); - + auto windowManager = MWBase::Environment::get().getWindowManager(); + bool wasOpen = windowManager->containsMode(MWGui::GM_Container); + windowManager->onDeleteCustomData(ptr); // HACK: disable/enable object to re-add it to the scene properly (need a new Animation). MWBase::Environment::get().getWorld()->disable(ptr); - // resets runtime state such as inventory, stats and AI. does not reset position in the world - ptr.getRefData().setCustomData(nullptr); + if (wasOpen && !windowManager->containsMode(MWGui::GM_Container)) + { + // Reopen the loot GUI if it was closed because we resurrected the actor we were looting + MWBase::Environment::get().getMechanicsManager()->resurrect(ptr); + windowManager->forceLootMode(ptr); + } + else + { + MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + // resets runtime state such as inventory, stats and AI. does not reset position in the world + ptr.getRefData().setCustomData(nullptr); + } if (wasEnabled) MWBase::Environment::get().getWorld()->enable(ptr); } diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 3d00b24ef8..292965fd94 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, false, false); + MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff); } template @@ -312,7 +312,7 @@ namespace MWScript } dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true, true)); + MWBase::Environment::get().getWorld()->moveObject(ptr, newPos, true, true)); } }; @@ -622,12 +622,13 @@ namespace MWScript runtime.pop(); auto rot = ptr.getRefData().getPosition().asRotationVec3(); - if (axis == "x") + // Regardless of the axis argument, the player may only be rotated on Z + if (axis == "z" || MWMechanics::getPlayer() == ptr) + rot.z() += rotation; + else if (axis == "x") rot.x() += rotation; else if (axis == "y") rot.y() += rotation; - else if (axis == "z") - rot.z() += rotation; MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); } }; @@ -731,7 +732,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, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; @@ -767,7 +768,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, false, true)); + MWBase::Environment::get().getWorld()->moveObjectBy(ptr, diff)); } }; diff --git a/apps/openmw/mwsound/loudness.cpp b/apps/openmw/mwsound/loudness.cpp index ae31d60949..a36615ee4a 100644 --- a/apps/openmw/mwsound/loudness.cpp +++ b/apps/openmw/mwsound/loudness.cpp @@ -40,7 +40,7 @@ void Sound_Loudness::analyzeLoudness(const std::vector< char >& data) else if (mSampleType == SampleType_Float32) { value = *reinterpret_cast(&mQueue[sample*advance]); - value = std::max(-1.f, std::min(1.f, value)); // Float samples *should* be scaled to [-1,1] already. + value = std::clamp(value, -1.f, 1.f); // Float samples *should* be scaled to [-1,1] already. } sum += value*value; @@ -64,8 +64,7 @@ float Sound_Loudness::getLoudnessAtTime(float sec) const if(mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; - size_t index = static_cast(sec * mSamplesPerSec); - index = std::max(0, std::min(index, mSamples.size()-1)); + size_t index = std::clamp(sec * mSamplesPerSec, 0, mSamples.size() - 1); return mSamples[index]; } diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 67b52309d5..b3cc81b803 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include "openal_output.hpp" @@ -954,17 +955,7 @@ std::pair OpenAL_Output::loadSound(const std::string &fname try { DecoderPtr decoder = mManager.getDecoder(); - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(decoder->mResourceMgr->exists(fname)) - decoder->open(fname); - else - { - std::string file = fname; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); - } + decoder->open(Misc::ResourceHelpers::correctSoundPath(fname, decoder->mResourceMgr)); ChannelConfig chans; SampleType type; @@ -1109,13 +1100,8 @@ void OpenAL_Output::initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat m alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } -void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d) +void OpenAL_Output::updateCommon(ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv) { - if(is3d) - { - if((pos - mListenerPos).length2() > maxdist*maxdist) - gain = 0.0f; - } if(useenv && mListenerEnv == Env_Underwater && !mWaterFilter) { gain *= 0.9f; @@ -1243,7 +1229,7 @@ void OpenAL_Output::updateSound(Sound *sound) ALuint source = GET_PTRID(sound->mHandle); updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); + sound->getPitch(), sound->getUseEnv()); getALError(); } @@ -1369,7 +1355,7 @@ void OpenAL_Output::updateStream(Stream *sound) ALuint source = stream->mSource; updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), - sound->getPitch(), sound->getUseEnv(), sound->getIs3D()); + sound->getPitch(), sound->getUseEnv()); getALError(); } diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openal_output.hpp index 2a19e6768a..47845c0802 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openal_output.hpp @@ -53,7 +53,7 @@ namespace MWSound void initCommon2D(ALuint source, const osg::Vec3f &pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv); void initCommon3D(ALuint source, const osg::Vec3f &pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv); - void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv, bool is3d); + void updateCommon(ALuint source, const osg::Vec3f &pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv); OpenAL_Output& operator=(const OpenAL_Output &rhs); OpenAL_Output(const OpenAL_Output &rhs); diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index 17f052aec0..2a07f05779 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -11,7 +11,11 @@ namespace MWSound enum PlayModeEx { Play_2D = 0, + Play_StopAtFadeEnd = 1 << 28, + Play_FadeExponential = 1 << 29, + Play_InFade = 1 << 30, Play_3D = 1 << 31, + Play_FadeFlagsMask = (Play_StopAtFadeEnd | Play_FadeExponential), }; // For testing individual PlayMode flags @@ -21,13 +25,15 @@ namespace MWSound struct SoundParams { osg::Vec3f mPos; - float mVolume = 1; - float mBaseVolume = 1; - float mPitch = 1; - float mMinDistance = 1; - float mMaxDistance = 1000; + float mVolume = 1.0f; + float mBaseVolume = 1.0f; + float mPitch = 1.0f; + float mMinDistance = 1.0f; + float mMaxDistance = 1000.0f; int mFlags = 0; - float mFadeOutTime = 0; + float mFadeVolume = 1.0f; + float mFadeTarget = 0.0f; + float mFadeStep = 0.0f; }; class SoundBase { @@ -46,19 +52,97 @@ namespace MWSound void setPosition(const osg::Vec3f &pos) { mParams.mPos = pos; } void setVolume(float volume) { mParams.mVolume = volume; } void setBaseVolume(float volume) { mParams.mBaseVolume = volume; } - void setFadeout(float duration) { mParams.mFadeOutTime = duration; } - void updateFade(float duration) + void setFadeout(float duration) { setFade(duration, 0.0, Play_StopAtFadeEnd); } + + /// Fade to the given linear gain within the specified amount of time. + /// Note that the fade gain is independent of the sound volume. + /// + /// \param duration specifies the duration of the fade. For *linear* + /// fades (default) this will be exactly the time at which the desired + /// volume is reached. Let v0 be the initial volume, v1 be the target + /// volume, and t0 be the initial time. Then the volume over time is + /// given as + /// + /// v(t) = v0 + (v1 - v0) * (t - t0) / duration if t <= t0 + duration + /// v(t) = v1 if t > t0 + duration + /// + /// For *exponential* fades this determines the time-constant of the + /// exponential process describing the fade. In particular, we guarantee + /// that we reach v0 + 0.99 * (v1 - v0) within the given duration. + /// + /// v(t) = v1 + (v0 - v1) * exp(-4.6 * (t0 - t) / duration) + /// + /// where -4.6 is approximately log(1%) (i.e., -40 dB). + /// + /// This interpolation mode is meant for environmental sound effects to + /// achieve less jarring transitions. + /// + /// \param targetVolume is the linear gain that should be reached at + /// the end of the fade. + /// + /// \param flags may be a combination of Play_FadeExponential and + /// Play_StopAtFadeEnd. If Play_StopAtFadeEnd is set, stops the sound + /// once the fade duration has passed or the target volume has been + /// reached. If Play_FadeExponential is set, enables the exponential + /// fade mode (see above). + void setFade(float duration, float targetVolume, int flags = 0) { + // Approximation of log(1%) (i.e., -40 dB). + constexpr float minus40Decibel = -4.6f; + + // Do nothing if already at the target, unless we need to trigger a stop event + if ((mParams.mFadeVolume == targetVolume) && !(flags & Play_StopAtFadeEnd)) + return; + + mParams.mFadeTarget = targetVolume; + mParams.mFlags = (mParams.mFlags & ~Play_FadeFlagsMask) | (flags & Play_FadeFlagsMask) | Play_InFade; + if (duration > 0.0f) + { + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeStep = -minus40Decibel / duration; + else + mParams.mFadeStep = (mParams.mFadeTarget - mParams.mFadeVolume) / duration; + } + else + { + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFadeStep = 0.0f; + } + } + + /// Updates the internal fading logic. + /// + /// \param dt is the time in seconds since the last call to update. + /// + /// \return true if the sound is still active, false if the sound has + /// reached a fading destination that was marked with Play_StopAtFadeEnd. + bool updateFade(float dt) { - if (mParams.mFadeOutTime > 0.0f) + // Mark fade as done at this volume difference (-80dB when fading to zero) + constexpr float minVolumeDifference = 1e-4f; + + if (!getInFade()) + return true; + + // Perform the actual fade operation + const float deltaBefore = mParams.mFadeTarget - mParams.mFadeVolume; + if (mParams.mFlags & Play_FadeExponential) + mParams.mFadeVolume += mParams.mFadeStep * deltaBefore * dt; + else + mParams.mFadeVolume += mParams.mFadeStep * dt; + const float deltaAfter = mParams.mFadeTarget - mParams.mFadeVolume; + + // Abort fade if we overshot or reached the minimum difference + if ((std::signbit(deltaBefore) != std::signbit(deltaAfter)) || (std::abs(deltaAfter) < minVolumeDifference)) { - float soundDuration = std::min(duration, mParams.mFadeOutTime); - mParams.mVolume *= (mParams.mFadeOutTime - soundDuration) / mParams.mFadeOutTime; - mParams.mFadeOutTime -= soundDuration; + mParams.mFadeVolume = mParams.mFadeTarget; + mParams.mFlags &= ~Play_InFade; } + + return getInFade() || !(mParams.mFlags & Play_StopAtFadeEnd); } const osg::Vec3f &getPosition() const { return mParams.mPos; } - float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume; } + float getRealVolume() const { return mParams.mVolume * mParams.mBaseVolume * mParams.mFadeVolume; } float getPitch() const { return mParams.mPitch; } float getMinDistance() const { return mParams.mMinDistance; } float getMaxDistance() const { return mParams.mMaxDistance; } @@ -69,6 +153,7 @@ namespace MWSound bool getIsLooping() const { return mParams.mFlags & MWSound::PlayMode::Loop; } bool getDistanceCull() const { return mParams.mFlags & MWSound::PlayMode::RemoveAtDistance; } bool getIs3D() const { return mParams.mFlags & Play_3D; } + bool getInFade() const { return mParams.mFlags & Play_InFade; } void init(const SoundParams& params) { diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index d2422870d7..5399b95c97 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -33,6 +34,8 @@ namespace MWSound namespace { constexpr float sMinUpdateInterval = 1.0f / 30.0f; + constexpr float sSfxFadeInDuration = 1.0f; + constexpr float sSfxFadeOutDuration = 1.0f; WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings() { @@ -47,6 +50,24 @@ namespace MWSound return settings; } + + float initialFadeVolume(float squaredDist, Sound_Buffer *sfx, Type type, PlayMode mode) + { + // If a sound is farther away than its maximum distance, start playing it with a zero fade volume. + // It can still become audible once the player moves closer. + const float maxDist = sfx->getMaxDist(); + if (squaredDist > (maxDist * maxDist)) + return 0.0f; + + // This is a *heuristic* that causes environment sounds to fade in. The idea is the following: + // - Only looped sounds playing through the effects channel are environment sounds + // - Do not fade in sounds if the player is already so close that the sound plays at maximum volume + const float minDist = sfx->getMinDist(); + if ((squaredDist > (minDist * minDist)) && (type == Type::Sfx) && (mode & PlayMode::Loop)) + return 0.0f; + + return 1.0; + } } // For combining PlayMode and Type flags @@ -125,19 +146,7 @@ namespace MWSound try { DecoderPtr decoder = getDecoder(); - - // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. - if(mVFS->exists(voicefile)) - decoder->open(voicefile); - else - { - std::string file = voicefile; - std::string::size_type pos = file.rfind('.'); - if(pos != std::string::npos) - file = file.substr(0, pos)+".mp3"; - decoder->open(file); - } - + decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, decoder->mResourceMgr)); return decoder; } catch(std::exception &e) @@ -224,7 +233,16 @@ namespace MWSound stopMusic(); DecoderPtr decoder = getDecoder(); - decoder->open(filename); + try + { + decoder->open(filename); + } + catch(std::exception &e) + { + Log(Debug::Error) << "Failed to load audio from " << filename << ": " << e.what(); + return; + } + mMusic = getStreamRef(); mMusic->init([&] { @@ -508,7 +526,8 @@ namespace MWSound return nullptr; const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); - if ((mode & PlayMode::RemoveAtDistance) && (mListenerPos - objpos).length2() > 2000 * 2000) + const float squaredDist = (mListenerPos - objpos).length2(); + if ((mode & PlayMode::RemoveAtDistance) && squaredDist > 2000 * 2000) return nullptr; // Look up the sound in the ESM data @@ -539,6 +558,7 @@ namespace MWSound params.mPos = objpos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); @@ -567,12 +587,15 @@ namespace MWSound Sound_Buffer *sfx = mSoundBuffers.load(Misc::StringUtils::lowerCase(soundId)); if(!sfx) return nullptr; + const float squaredDist = (mListenerPos - initialPos).length2(); + SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; params.mPos = initialPos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); + params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); @@ -768,10 +791,10 @@ namespace MWSound break; case WaterSoundAction::SetVolume: mNearWaterSound->setVolume(update.mVolume * sfx->getVolume()); + mNearWaterSound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); break; case WaterSoundAction::FinishSound: - mOutput->finishSound(mNearWaterSound); - mNearWaterSound = nullptr; + mNearWaterSound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | Play_StopAtFadeEnd); break; case WaterSoundAction::PlaySound: if (mNearWaterSound) @@ -821,6 +844,28 @@ namespace MWSound return {WaterSoundAction::DoNothing, nullptr}; } + void SoundManager::cull3DSound(SoundBase *sound) + { + // Hard-coded distance of 2000.0f is from vanilla Morrowind + const float maxDist = sound->getDistanceCull() ? 2000.0f : sound->getMaxDistance(); + const float squaredMaxDist = maxDist * maxDist; + + const osg::Vec3f pos = sound->getPosition(); + const float squaredDist = (mListenerPos - pos).length2(); + + if (squaredDist > squaredMaxDist) + { + // If getDistanceCull() is set, delete the sound after it has faded out + sound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | (sound->getDistanceCull() ? Play_StopAtFadeEnd : 0)); + } + else + { + // Fade sounds back in once they are in range + sound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); + } + } + + void SoundManager::updateSounds(float duration) { // We update active say sounds map for specific actors here @@ -875,20 +920,15 @@ namespace MWSound { Sound *sound = sndidx->first.get(); - if(!ptr.isEmpty() && sound->getIs3D()) + if (sound->getIs3D()) { - const ESM::Position &pos = ptr.getRefData().getPosition(); - const osg::Vec3f objpos(pos.asVec3()); - sound->setPosition(objpos); - - if(sound->getDistanceCull()) - { - if((mListenerPos - objpos).length2() > 2000*2000) - mOutput->finishSound(sound); - } + if (!ptr.isEmpty()) + sound->setPosition(ptr.getRefData().getPosition().asVec3()); + + cull3DSound(sound); } - if(!mOutput->isSoundPlaying(sound)) + if(!sound->updateFade(duration) || !mOutput->isSoundPlaying(sound)) { mOutput->finishSound(sound); if (sound == mUnderwaterSound) @@ -900,8 +940,6 @@ namespace MWSound } else { - sound->updateFade(duration); - mOutput->updateSound(sound); ++sndidx; } @@ -917,28 +955,24 @@ namespace MWSound { MWWorld::ConstPtr ptr = sayiter->first; Stream *sound = sayiter->second.get(); - if(!ptr.isEmpty() && sound->getIs3D()) + if (sound->getIs3D()) { - MWBase::World *world = MWBase::Environment::get().getWorld(); - const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); - sound->setPosition(pos); - - if(sound->getDistanceCull()) + if (!ptr.isEmpty()) { - if((mListenerPos - pos).length2() > 2000*2000) - mOutput->finishStream(sound); + MWBase::World *world = MWBase::Environment::get().getWorld(); + sound->setPosition(world->getActorHeadTransform(ptr).getTrans()); } + + cull3DSound(sound); } - if(!mOutput->isStreamPlaying(sound)) + if(!sound->updateFade(duration) || !mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); - mActiveSaySounds.erase(sayiter++); + sayiter = mActiveSaySounds.erase(sayiter); } else { - sound->updateFade(duration); - mOutput->updateStream(sound); ++sayiter; } diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 934402cd4c..e659e7a12a 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -34,6 +34,7 @@ namespace MWSound { class Sound_Output; struct Sound_Decoder; + class SoundBase; class Sound; class Stream; @@ -111,6 +112,8 @@ namespace MWSound void advanceMusic(const std::string& filename); void startRandomTitle(); + void cull3DSound(SoundBase *sound); + void updateSounds(float duration); void updateRegionSound(float duration); void updateWaterSound(); diff --git a/apps/openmw/mwsound/volumesettings.cpp b/apps/openmw/mwsound/volumesettings.cpp index cc4eac3d6d..fd79b97e9b 100644 --- a/apps/openmw/mwsound/volumesettings.cpp +++ b/apps/openmw/mwsound/volumesettings.cpp @@ -10,7 +10,7 @@ namespace MWSound { float clamp(float value) { - return std::max(0.0f, std::min(1.0f, value)); + return std::clamp(value, 0.f, 1.f); } } diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index df1ab1bdfb..52696de104 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -8,11 +8,22 @@ #include #include +#include + bool MWState::operator< (const Slot& left, const Slot& right) { return left.mTimeStamp& contentFiles) +{ + for (const std::string& c : contentFiles) + { + if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) + return c; + } + return ""; +} void MWState::Character::addSlot (const boost::filesystem::path& path, const std::string& game) { @@ -30,8 +41,7 @@ void MWState::Character::addSlot (const boost::filesystem::path& path, const std slot.mProfile.load (reader); - if (Misc::StringUtils::lowerCase (slot.mProfile.mContentFiles.at (0))!= - Misc::StringUtils::lowerCase (game)) + if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore mSlots.push_back (slot); @@ -44,12 +54,14 @@ void MWState::Character::addSlot (const ESM::SavedGame& profile) std::ostringstream stream; // The profile description is user-supplied, so we need to escape the path - for (std::string::const_iterator it = profile.mDescription.begin(); it != profile.mDescription.end(); ++it) + Utf8Stream description(profile.mDescription); + while(!description.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = description.consume(); + if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } const std::string ext = ".omwsave"; diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 32c79a183e..e12de9ca64 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -16,6 +16,8 @@ namespace MWState bool operator< (const Slot& left, const Slot& right); + std::string getFirstGameFile(const std::vector& contentFiles); + class Character { public: diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index a324dfe0f7..301f33c5df 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -5,9 +5,11 @@ #include +#include + MWState::CharacterManager::CharacterManager (const boost::filesystem::path& saves, - const std::string& game) -: mPath (saves), mCurrent (nullptr), mGame (game) + const std::vector& contentFiles) +: mPath (saves), mCurrent (nullptr), mGame (getFirstGameFile(contentFiles)) { if (!boost::filesystem::is_directory (mPath)) { @@ -57,12 +59,14 @@ MWState::Character* MWState::CharacterManager::createCharacter(const std::string std::ostringstream stream; // The character name is user-supplied, so we need to escape the path - for (std::string::const_iterator it = name.begin(); it != name.end(); ++it) + Utf8Stream nameStream(name); + while(!nameStream.eof()) { - if (std::isalnum(*it)) // Ignores multibyte characters and non alphanumeric characters - stream << *it; + auto c = nameStream.consume(); + if(c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters + stream << static_cast(c); else - stream << "_"; + stream << '_'; } boost::filesystem::path path = mPath / stream.str(); diff --git a/apps/openmw/mwstate/charactermanager.hpp b/apps/openmw/mwstate/charactermanager.hpp index 2daf73401f..8b3f2b8f8f 100644 --- a/apps/openmw/mwstate/charactermanager.hpp +++ b/apps/openmw/mwstate/charactermanager.hpp @@ -29,7 +29,7 @@ namespace MWState public: - CharacterManager (const boost::filesystem::path& saves, const std::string& game); + CharacterManager (const boost::filesystem::path& saves, const std::vector& contentFiles); Character *getCurrentCharacter (); ///< @note May return null diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 4e4a8e95d6..b9825a0f90 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -88,8 +88,8 @@ std::map MWState::StateManager::buildContentFileIndexMap (const ESM::E return map; } -MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::string& game) -: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, game), mTimePlayed (0) +MWState::StateManager::StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles) +: mQuitRequest (false), mAskLoadRecent(false), mState (State_NoGame), mCharacterManager (saves, contentFiles), mTimePlayed (0) { } @@ -406,7 +406,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str ESM::NAME n = reader.getRecName(); reader.getRecHeader(); - switch (n.intval) + switch (n.toInt()) { case ESM::REC_SAVE: { @@ -427,12 +427,12 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_JOUR_LEGACY: case ESM::REC_QUES: - MWBase::Environment::get().getJournal()->readRecord (reader, n.intval); + MWBase::Environment::get().getJournal()->readRecord (reader, n.toInt()); break; case ESM::REC_DIAS: - MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.intval); + MWBase::Environment::get().getDialogueManager()->readRecord (reader, n.toInt()); break; case ESM::REC_ALCH: @@ -457,7 +457,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_LEVI: case ESM::REC_CREA: case ESM::REC_CONT: - MWBase::Environment::get().getWorld()->readRecord(reader, n.intval, contentFileMap); + MWBase::Environment::get().getWorld()->readRecord(reader, n.toInt(), contentFileMap); break; case ESM::REC_CAM_: @@ -466,7 +466,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_GSCR: - MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.intval, contentFileMap); + MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord (reader, n.toInt(), contentFileMap); break; case ESM::REC_GMAP: @@ -474,21 +474,21 @@ void MWState::StateManager::loadGame (const Character *character, const std::str case ESM::REC_ASPL: case ESM::REC_MARK: - MWBase::Environment::get().getWindowManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getWindowManager()->readRecord(reader, n.toInt()); break; case ESM::REC_DCOU: case ESM::REC_STLN: - MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.toInt()); break; case ESM::REC_INPU: - MWBase::Environment::get().getInputManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getInputManager()->readRecord(reader, n.toInt()); break; case ESM::REC_LUAM: - MWBase::Environment::get().getLuaManager()->readRecord(reader, n.intval); + MWBase::Environment::get().getLuaManager()->readRecord(reader, n.toInt()); break; default: @@ -558,6 +558,8 @@ void MWState::StateManager::loadGame (const Character *character, const std::str // Since we passed "changeEvent=false" to changeCell, we shouldn't have triggered the cell change flag. // But make sure the flag is cleared anyway in case it was set from an earlier game. MWBase::Environment::get().getWorld()->markCellAsUnchanged(); + + MWBase::Environment::get().getLuaManager()->gameLoaded(); } catch (const std::exception& e) { diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index 3534dabf2b..a29e72b3ad 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -31,7 +31,7 @@ namespace MWState public: - StateManager (const boost::filesystem::path& saves, const std::string& game); + StateManager (const boost::filesystem::path& saves, const std::vector& contentFiles); void requestQuit() override; diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 590e1e9c00..fc09a6e9a8 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -322,11 +321,10 @@ namespace MWWorld PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { - // do the deletion in the background thread if (found->second.mWorkItem) { found->second.mWorkItem->abort(); - mUnrefQueue->push(std::move(found->second.mWorkItem)); + found->second.mWorkItem = nullptr; } mPreloadCells.erase(found); @@ -340,7 +338,7 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); @@ -356,7 +354,7 @@ namespace MWWorld if (it->second.mWorkItem) { it->second.mWorkItem->abort(); - mUnrefQueue->push(it->second.mWorkItem); + it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); } @@ -409,11 +407,6 @@ namespace MWWorld mWorkQueue = workQueue; } - void CellPreloader::setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue) - { - mUnrefQueue = unrefQueue; - } - bool CellPreloader::syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener) { if (!mTerrainPreloadItem) @@ -455,11 +448,7 @@ namespace MWWorld else { if (mTerrainViews.size() > positions.size()) - { - for (unsigned int i=positions.size(); ipush(mTerrainViews[i]); mTerrainViews.resize(positions.size()); - } else if (mTerrainViews.size() < positions.size()) { for (unsigned int i=mTerrainViews.size(); i workQueue); - void setUnrefQueue(SceneUtil::UnrefQueue* unrefQueue); - typedef std::pair PositionCellGrid; void setTerrainPreloadPositions(const std::vector& positions); @@ -87,7 +80,6 @@ namespace MWWorld Terrain::World* mTerrain; MWRender::LandManager* mLandManager; osg::ref_ptr mWorkQueue; - osg::ref_ptr mUnrefQueue; double mExpiryDelay; unsigned int mMinCacheSize; unsigned int mMaxCacheSize; diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 0b16964043..2f4702b1eb 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -8,11 +8,6 @@ namespace MWWorld { - const ESM::RefNum& CellRef::getRefNum() const - { - return mCellRef.mRefNum; - } - const ESM::RefNum& CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) { if (!mCellRef.mRefNum.isSet()) @@ -33,41 +28,11 @@ namespace MWWorld return mCellRef.mRefNum; } - bool CellRef::hasContentFile() const - { - return mCellRef.mRefNum.hasContentFile(); - } - void CellRef::unsetRefNum() { mCellRef.mRefNum.unset(); } - std::string CellRef::getRefId() const - { - return mCellRef.mRefID; - } - - bool CellRef::getTeleport() const - { - return mCellRef.mTeleport; - } - - ESM::Position CellRef::getDoorDest() const - { - return mCellRef.mDoorDest; - } - - std::string CellRef::getDestCell() const - { - return mCellRef.mDestCell; - } - - float CellRef::getScale() const - { - return mCellRef.mScale; - } - void CellRef::setScale(float scale) { if (scale != mCellRef.mScale) @@ -77,22 +42,12 @@ namespace MWWorld } } - ESM::Position CellRef::getPosition() const - { - return mCellRef.mPos; - } - void CellRef::setPosition(const ESM::Position &position) { mChanged = true; mCellRef.mPos = position; } - float CellRef::getEnchantmentCharge() const - { - return mCellRef.mEnchantmentCharge; - } - float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const { if (maxCharge == 0) @@ -118,11 +73,6 @@ namespace MWWorld } } - int CellRef::getCharge() const - { - return mCellRef.mChargeInt; - } - void CellRef::setCharge(int charge) { if (charge != mCellRef.mChargeInt) @@ -150,11 +100,6 @@ namespace MWWorld } } - float CellRef::getChargeFloat() const - { - return mCellRef.mChargeFloat; - } - void CellRef::setChargeFloat(float charge) { if (charge != mCellRef.mChargeFloat) @@ -164,16 +109,6 @@ namespace MWWorld } } - std::string CellRef::getOwner() const - { - return mCellRef.mOwner; - } - - std::string CellRef::getGlobalVariable() const - { - return mCellRef.mGlobalVariable; - } - void CellRef::resetGlobalVariable() { if (!mCellRef.mGlobalVariable.empty()) @@ -192,11 +127,6 @@ namespace MWWorld } } - int CellRef::getFactionRank() const - { - return mCellRef.mFactionRank; - } - void CellRef::setOwner(const std::string &owner) { if (owner != mCellRef.mOwner) @@ -206,11 +136,6 @@ namespace MWWorld } } - std::string CellRef::getSoul() const - { - return mCellRef.mSoul; - } - void CellRef::setSoul(const std::string &soul) { if (soul != mCellRef.mSoul) @@ -220,11 +145,6 @@ namespace MWWorld } } - std::string CellRef::getFaction() const - { - return mCellRef.mFaction; - } - void CellRef::setFaction(const std::string &faction) { if (faction != mCellRef.mFaction) @@ -234,11 +154,6 @@ namespace MWWorld } } - int CellRef::getLockLevel() const - { - return mCellRef.mLockLevel; - } - void CellRef::setLockLevel(int lockLevel) { if (lockLevel != mCellRef.mLockLevel) @@ -261,16 +176,6 @@ namespace MWWorld setLockLevel(-abs(mCellRef.mLockLevel)); //Makes lockLevel negative } - std::string CellRef::getKey() const - { - return mCellRef.mKey; - } - - std::string CellRef::getTrap() const - { - return mCellRef.mTrap; - } - void CellRef::setTrap(const std::string& trap) { if (trap != mCellRef.mTrap) @@ -280,11 +185,6 @@ namespace MWWorld } } - int CellRef::getGoldValue() const - { - return mCellRef.mGoldValue; - } - void CellRef::setGoldValue(int value) { if (value != mCellRef.mGoldValue) @@ -299,9 +199,4 @@ namespace MWWorld state.mRef = mCellRef; } - bool CellRef::hasChanged() const - { - return mChanged; - } - } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 78170a766f..b5e80930ed 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -23,7 +23,7 @@ namespace MWWorld } // Note: Currently unused for items in containers - const ESM::RefNum& getRefNum() const; + const ESM::RefNum& getRefNum() const { return mCellRef.mRefNum; } // Returns RefNum. // If RefNum is not set, assigns a generated one and changes the "lastAssignedRefNum" counter. @@ -33,35 +33,35 @@ namespace MWWorld void unsetRefNum(); /// Does the RefNum have a content file? - bool hasContentFile() const; + bool hasContentFile() const { return mCellRef.mRefNum.hasContentFile(); } // Id of object being referenced - std::string getRefId() const; + const std::string& getRefId() const { return mCellRef.mRefID; } // Reference to ID of the object being referenced - const std::string& getRefIdRef() const { return mCellRef.mRefID; } + const std::string& getRefIdRef() const { return mCellRef.mRefID; } // TODO replace with getRefId // For doors - true if this door teleports to somewhere else, false // if it should open through animation. - bool getTeleport() const; + bool getTeleport() const { return mCellRef.mTeleport; } // Teleport location for the door, if this is a teleporting door. - ESM::Position getDoorDest() const; + const ESM::Position& getDoorDest() const { return mCellRef.mDoorDest; } // Destination cell for doors (optional) - std::string getDestCell() const; + const std::string& getDestCell() const { return mCellRef.mDestCell; } // Scale applied to mesh - float getScale() const; + float getScale() const { return mCellRef.mScale; } void setScale(float scale); // The *original* position and rotation as it was given in the Construction Set. // Current position and rotation of the object is stored in RefData. - ESM::Position getPosition() const; + const ESM::Position& getPosition() const { return mCellRef.mPos; } void setPosition (const ESM::Position& position); // Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full). - float getEnchantmentCharge() const; + float getEnchantmentCharge() const { return mCellRef.mEnchantmentCharge; } // Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment). float getNormalizedEnchantmentCharge(int maxCharge) const; @@ -71,57 +71,57 @@ namespace MWWorld // For weapon or armor, this is the remaining item health. // For tools (lockpicks, probes, repair hammer) it is the remaining uses. // If this returns int(-1) it means full health. - int getCharge() const; - float getChargeFloat() const; // Implemented as union with int charge + int getCharge() const { return mCellRef.mChargeInt; } + float getChargeFloat() const { return mCellRef.mChargeFloat; } // Implemented as union with int charge void setCharge(int charge); void setChargeFloat(float charge); void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1 // The NPC that owns this object (and will get angry if you steal it) - std::string getOwner() const; + const std::string& getOwner() const { return mCellRef.mOwner; } void setOwner(const std::string& owner); // Name of a global variable. If the global variable is set to '1', using the object is temporarily allowed // even if it has an Owner field. // Used by bed rent scripts to allow the player to use the bed for the duration of the rent. - std::string getGlobalVariable() const; + const std::string& getGlobalVariable() const { return mCellRef.mGlobalVariable; } void resetGlobalVariable(); // ID of creature trapped in this soul gem - std::string getSoul() const; + const std::string& getSoul() const { return mCellRef.mSoul; } void setSoul(const std::string& soul); // The faction that owns this object (and will get angry if // you take it and are not a faction member) - std::string getFaction() const; + const std::string& getFaction() const { return mCellRef.mFaction; } void setFaction (const std::string& faction); // PC faction rank required to use the item. Sometimes is -1, which means "any rank". void setFactionRank(int factionRank); - int getFactionRank() const; + int getFactionRank() const { return mCellRef.mFactionRank; } // Lock level for doors and containers // Positive for a locked door. 0 for a door that was never locked. // For an unlocked door, it is set to -(previous locklevel) - int getLockLevel() const; + int getLockLevel() const { return mCellRef.mLockLevel; } void setLockLevel(int lockLevel); void lock(int lockLevel); void unlock(); // Key and trap ID names, if any - std::string getKey() const; - std::string getTrap() const; + const std::string& getKey() const { return mCellRef.mKey; } + const std::string& getTrap() const { return mCellRef.mTrap; } void setTrap(const std::string& trap); // This is 5 for Gold_005 references, 100 for Gold_100 and so on. - int getGoldValue() const; + int getGoldValue() const { return mCellRef.mGoldValue; } void setGoldValue(int value); // Write the content of this CellRef into the given ObjectState void writeState (ESM::ObjectState& state) const; // Has this CellRef changed since it was originally loaded? - bool hasChanged() const; + bool hasChanged() const { return mChanged; } private: bool mChanged; diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index b2ac511509..0c871e4f58 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1,4 +1,5 @@ #include "cellstore.hpp" +#include "magiceffects.hpp" #include @@ -741,11 +742,7 @@ namespace MWWorld case ESM::REC_NPC_: mNpcs.load(ref, deleted, store); break; case ESM::REC_PROB: mProbes.load(ref, deleted, store); break; case ESM::REC_REPA: mRepairs.load(ref, deleted, store); break; - case ESM::REC_STAT: - { - if (ref.mRefNum.fromGroundcoverFile()) return; - mStatics.load(ref, deleted, store); break; - } + case ESM::REC_STAT: mStatics.load(ref, deleted, store); break; case ESM::REC_WEAP: mWeapons.load(ref, deleted, store); break; case ESM::REC_BODY: mBodyParts.load(ref, deleted, store); break; @@ -1198,4 +1195,18 @@ namespace MWWorld || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) mRechargingItems.emplace_back(ptr.getBase(), static_cast(enchantment->mData.mCharge)); } + + Ptr MWWorld::CellStore::getMovedActor(int actorId) const + { + for(const auto& [cellRef, cell] : mMovedToAnotherCell) + { + if(cellRef->mClass->isActor() && cellRef->mData.getCustomData()) + { + Ptr actor(cellRef, cell); + if(actor.getClass().getCreatureStats(actor).getActorId() == actorId) + return actor; + } + } + return {}; + } } diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 6e927fbea6..d284a291a5 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -397,6 +397,8 @@ namespace MWWorld void respawn (); ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. + Ptr getMovedActor(int actorId) const; + private: /// Run through references and store IDs diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index da4dd9d99e..24cd09ad37 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -23,7 +23,7 @@ namespace MWWorld { - std::map > Class::sClasses; + std::map > Class::sClasses; void Class::insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const { @@ -228,15 +228,12 @@ namespace MWWorld throw std::runtime_error("Class does not support armor rating"); } - const Class& Class::get (const std::string& key) + const Class& Class::get (unsigned int key) { - if (key.empty()) - throw std::logic_error ("Class::get(): attempting to get an empty key"); - - std::map >::const_iterator iter = sClasses.find (key); + auto iter = sClasses.find (key); if (iter==sClasses.end()) - throw std::logic_error ("Class::get(): unknown class key: " + key); + throw std::logic_error ("Class::get(): unknown class key: " + std::to_string(key)); return *iter->second; } @@ -246,9 +243,9 @@ namespace MWWorld throw std::runtime_error ("class does not support persistence"); } - void Class::registerClass(const std::string& key, std::shared_ptr instance) + void Class::registerClass(unsigned int key, std::shared_ptr instance) { - instance->mTypeName = key; + instance->mType = key; sClasses.insert(std::make_pair(key, instance)); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 8e451ea580..64ac873960 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -54,9 +54,8 @@ namespace MWWorld /// \brief Base class for referenceable esm records class Class { - static std::map > sClasses; - - std::string mTypeName; + static std::map > sClasses; + unsigned int mType; protected: @@ -73,8 +72,8 @@ namespace MWWorld Class (const Class&) = delete; Class& operator= (const Class&) = delete; - const std::string& getTypeName() const { - return mTypeName; + unsigned int getType() const { + return mType; } virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; @@ -338,10 +337,10 @@ namespace MWWorld const; ///< Write additional state from \a ptr into \a state. - static const Class& get (const std::string& key); + static const Class& get (unsigned int key); ///< If there is no class for this \a key, an exception is thrown. - static void registerClass (const std::string& key, std::shared_ptr instance); + static void registerClass (unsigned int key, std::shared_ptr instance); virtual int getBaseGold(const MWWorld::ConstPtr& ptr) const; diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index b02c2bb407..112f56abfc 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -1,7 +1,6 @@ #include "containerstore.hpp" #include -#include #include #include @@ -567,7 +566,7 @@ void MWWorld::ContainerStore::addInitialItem (const std::string& id, const std:: void MWWorld::ContainerStore::addInitialItemImp(const MWWorld::Ptr& ptr, const std::string& owner, int count, Misc::Rng::Seed* seed, bool topLevel) { - if (ptr.getTypeName()==typeid (ESM::ItemLevList).name()) + if (ptr.getType()==ESM::ItemLevList::sRecordId) { if(!seed) return; @@ -693,44 +692,44 @@ int MWWorld::ContainerStore::getType (const ConstPtr& ptr) if (ptr.isEmpty()) throw std::runtime_error ("can't put a non-existent object into a container"); - if (ptr.getTypeName()==typeid (ESM::Potion).name()) + if (ptr.getType()==ESM::Potion::sRecordId) return Type_Potion; - if (ptr.getTypeName()==typeid (ESM::Apparatus).name()) + if (ptr.getType()==ESM::Apparatus::sRecordId) return Type_Apparatus; - if (ptr.getTypeName()==typeid (ESM::Armor).name()) + if (ptr.getType()==ESM::Armor::sRecordId) return Type_Armor; - if (ptr.getTypeName()==typeid (ESM::Book).name()) + if (ptr.getType()==ESM::Book::sRecordId) return Type_Book; - if (ptr.getTypeName()==typeid (ESM::Clothing).name()) + if (ptr.getType()==ESM::Clothing::sRecordId) return Type_Clothing; - if (ptr.getTypeName()==typeid (ESM::Ingredient).name()) + if (ptr.getType()==ESM::Ingredient::sRecordId) return Type_Ingredient; - if (ptr.getTypeName()==typeid (ESM::Light).name()) + if (ptr.getType()==ESM::Light::sRecordId) return Type_Light; - if (ptr.getTypeName()==typeid (ESM::Lockpick).name()) + if (ptr.getType()==ESM::Lockpick::sRecordId) return Type_Lockpick; - if (ptr.getTypeName()==typeid (ESM::Miscellaneous).name()) + if (ptr.getType()==ESM::Miscellaneous::sRecordId) return Type_Miscellaneous; - if (ptr.getTypeName()==typeid (ESM::Probe).name()) + if (ptr.getType()==ESM::Probe::sRecordId) return Type_Probe; - if (ptr.getTypeName()==typeid (ESM::Repair).name()) + if (ptr.getType()==ESM::Repair::sRecordId) return Type_Repair; - if (ptr.getTypeName()==typeid (ESM::Weapon).name()) + if (ptr.getType()==ESM::Weapon::sRecordId) return Type_Weapon; - throw std::runtime_error ( - "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); + throw std::runtime_error("Object '" + ptr.getCellRef().getRefId() + "' of type " + + std::string(ptr.getTypeDescription()) + " can not be placed into a container"); } MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) diff --git a/apps/openmw/mwworld/contentloader.hpp b/apps/openmw/mwworld/contentloader.hpp index b529ae9db8..55de77ad25 100644 --- a/apps/openmw/mwworld/contentloader.hpp +++ b/apps/openmw/mwworld/contentloader.hpp @@ -2,33 +2,20 @@ #define CONTENTLOADER_HPP #include -#include -#include -#include "components/loadinglistener/loadinglistener.hpp" +namespace Loading +{ + class Listener; +} namespace MWWorld { struct ContentLoader { - ContentLoader(Loading::Listener& listener) - : mListener(listener) - { - } - - virtual ~ContentLoader() - { - } - - virtual void load(const boost::filesystem::path& filepath, int& index) - { - Log(Debug::Info) << "Loading content file " << filepath.string(); - mListener.setLabel(MyGUI::TextIterator::toTagsString(filepath.string())); - } + virtual ~ContentLoader() = default; - protected: - Loading::Listener& mListener; + virtual void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) = 0; }; } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index a01128fe36..de16e386f2 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -2,233 +2,27 @@ #include "esmstore.hpp" #include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwmechanics/magiceffects.hpp" - -namespace -{ - template - void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName) - { - const T* item = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if(item) - { - enchantment = item->mEnchant; - itemName = item->mName; - } - } -} namespace MWWorld { EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener) - : ContentLoader(listener) - , mEsm(readers) - , mStore(store) - , mEncoder(encoder) + ToUTF8::Utf8Encoder* encoder) + : mEsm(readers) + , mStore(store) + , mEncoder(encoder) { } -void EsmLoader::load(const boost::filesystem::path& filepath, int& index) +void EsmLoader::load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) { - ContentLoader::load(filepath.filename(), index); - - ESM::ESMReader lEsm; - lEsm.setEncoder(mEncoder); - lEsm.setIndex(index); - lEsm.setGlobalReaderList(&mEsm); - lEsm.open(filepath.string()); - mEsm[index] = lEsm; - mStore.load(mEsm[index], &mListener); + ESM::ESMReader lEsm; + lEsm.setEncoder(mEncoder); + lEsm.setIndex(index); + lEsm.open(filepath.string()); + lEsm.resolveParentFileIndices(mEsm); + mEsm[index] = lEsm; + mStore.load(mEsm[index], listener); } - void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) - { - const auto& store = MWBase::Environment::get().getWorld()->getStore(); - // Convert corprus to format 10 - for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) - { - const ESM::Spell* spell = store.get().search(id); - if (!spell) - continue; - - ESM::CreatureStats::CorprusStats stats; - stats.mNextWorsening = oldStats.mNextWorsening; - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; - } - creatureStats.mCorprusSpells[id] = stats; - } - // Convert to format 17 - for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) - { - const ESM::Spell* spell = store.get().search(id); - if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) - continue; - ESM::ActiveSpells::ActiveSpellParams params; - params.mId = id; - params.mDisplayName = spell->mName; - params.mItem.unset(); - params.mCasterActorId = creatureStats.mActorId; - if(spell->mData.mType == ESM::Spell::ST_Ability) - params.mType = ESM::ActiveSpells::Type_Ability; - else - params.mType = ESM::ActiveSpells::Type_Permanent; - params.mWorsenings = -1; - int effectIndex = 0; - for(const auto& enam : spell->mEffects.mList) - { - if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) - { - ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; - effect.mDuration = -1; - effect.mTimeLeft = -1; - effect.mEffectIndex = effectIndex; - auto rand = oldParams.mEffectRands.find(effectIndex); - if(rand != oldParams.mEffectRands.end()) - { - float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; - effect.mMagnitude = magnitude; - effect.mMinMagnitude = magnitude; - effect.mMaxMagnitude = magnitude; - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; - } - else - { - effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; - effect.mFlags = ESM::ActiveEffect::Flag_None; - } - params.mEffects.emplace_back(effect); - } - effectIndex++; - } - creatureStats.mActiveSpells.mSpells.emplace_back(params); - } - std::multimap equippedItems; - for(std::size_t i = 0; i < inventory.mItems.size(); ++i) - { - const ESM::ObjectState& item = inventory.mItems[i]; - auto slot = inventory.mEquipmentSlots.find(i); - if(slot != inventory.mEquipmentSlots.end()) - equippedItems.emplace(item.mRef.mRefID, slot->second); - } - for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) - { - std::string eId; - std::string name; - switch(store.find(id)) - { - case ESM::REC_ARMO: - getEnchantedItem(id, eId, name); - break; - case ESM::REC_CLOT: - getEnchantedItem(id, eId, name); - break; - case ESM::REC_WEAP: - getEnchantedItem(id, eId, name); - break; - } - if(eId.empty()) - continue; - const ESM::Enchantment* enchantment = store.get().search(eId); - if(!enchantment) - continue; - ESM::ActiveSpells::ActiveSpellParams params; - params.mId = id; - params.mDisplayName = name; - params.mCasterActorId = creatureStats.mActorId; - params.mType = ESM::ActiveSpells::Type_Enchantment; - params.mWorsenings = -1; - for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) - { - const auto& enam = enchantment->mEffects.mList[effectIndex]; - auto [random, multiplier] = oldMagnitudes[effectIndex]; - float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; - magnitude *= multiplier; - if(magnitude <= 0) - continue; - ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mMagnitude = magnitude; - effect.mMinMagnitude = magnitude; - effect.mMaxMagnitude = magnitude; - effect.mArg = MWMechanics::EffectKey(enam).mArg; - effect.mDuration = -1; - effect.mTimeLeft = -1; - effect.mEffectIndex = static_cast(effectIndex); - // Prevent recalculation of resistances - effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances; - params.mEffects.emplace_back(effect); - } - auto [begin, end] = equippedItems.equal_range(id); - for(auto it = begin; it != end; ++it) - { - params.mItem = { static_cast(it->second), 0 }; - creatureStats.mActiveSpells.mSpells.emplace_back(params); - } - } - for(const auto& spell : creatureStats.mCorprusSpells) - { - auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; }); - if(it != creatureStats.mActiveSpells.mSpells.end()) - { - it->mNextWorsening = spell.second.mNextWorsening; - int worsenings = 0; - for(int i = 0; i < ESM::Attribute::Length; ++i) - worsenings = std::max(spell.second.mWorsenings[i], worsenings); - it->mWorsenings = worsenings; - } - } - for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) - { - if(actorId == -1) - continue; - for(auto& params : creatureStats.mActiveSpells.mSpells) - { - if(params.mId == key.mSourceId) - { - bool found = false; - for(auto& effect : params.mEffects) - { - if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) - { - effect.mArg = actorId; - found = true; - break; - } - } - if(found) - break; - } - } - } - // Reset modifiers that were previously recalculated each frame - for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) - creatureStats.mAttributes[i].mMod = 0.f; - for(std::size_t i = 0; i < 3; ++i) - creatureStats.mDynamic[i].mMod = 0.f; - for(std::size_t i = 0; i < 4; ++i) - creatureStats.mAiSettings[i].mMod = 0.f; - if(npcStats) - { - for(std::size_t i = 0; i < ESM::Skill::Length; ++i) - npcStats->mSkills[i].mMod = 0.f; - } - } } /* namespace MWWorld */ diff --git a/apps/openmw/mwworld/esmloader.hpp b/apps/openmw/mwworld/esmloader.hpp index 50631603de..db50d44146 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -13,9 +13,6 @@ namespace ToUTF8 namespace ESM { class ESMReader; - struct CreatureStats; - struct InventoryState; - struct NpcStats; } namespace MWWorld @@ -26,18 +23,16 @@ class ESMStore; struct EsmLoader : public ContentLoader { EsmLoader(MWWorld::ESMStore& store, std::vector& readers, - ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener); + ToUTF8::Utf8Encoder* encoder); - void load(const boost::filesystem::path& filepath, int& index) override; + void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override; private: - std::vector& mEsm; - MWWorld::ESMStore& mStore; - ToUTF8::Utf8Encoder* mEncoder; + std::vector& mEsm; + MWWorld::ESMStore& mStore; + ToUTF8::Utf8Encoder* mEncoder; }; -void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr); - } /* namespace MWWorld */ #endif // ESMLOADER_HPP diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index befe65d641..1284df694a 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -1,14 +1,14 @@ #include "esmstore.hpp" #include +#include #include -#include - #include -#include #include #include +#include +#include #include #include "../mwmechanics/spelllist.hpp" @@ -27,6 +27,7 @@ namespace void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) { + // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { size_t index = cell.mContextList[i].index; @@ -59,7 +60,7 @@ namespace } } - std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::map& npcs) + std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found std::string defaultCls; @@ -112,8 +113,8 @@ namespace // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no longer exists however. // So instead of removing the item altogether, we're only removing the script. - template - void removeMissingScripts(const MWWorld::Store& scripts, std::map& items) + template + void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) { for(auto& [id, item] : items) { @@ -150,38 +151,9 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) ESM::Dialogue *dialogue = nullptr; // Land texture loading needs to use a separate internal store for each plugin. - // We set the number of plugins here to avoid continual resizes during loading, - // and so we can properly verify if valid plugin indices are being passed to the - // LandTexture Store retrieval methods. - mLandTextures.resize(esm.getGlobalReaderList()->size()); - - /// \todo Move this to somewhere else. ESMReader? - // Cache parent esX files by tracking their indices in the global list of - // all files/readers used by the engine. This will greaty accelerate - // refnumber mangling, as required for handling moved references. - const std::vector &masters = esm.getGameFiles(); - std::vector *allPlugins = esm.getGlobalReaderList(); - for (size_t j = 0; j < masters.size(); j++) { - const ESM::Header::MasterData &mast = masters[j]; - std::string fname = mast.name; - int index = ~0; - for (int i = 0; i < esm.getIndex(); i++) { - const std::string candidate = allPlugins->at(i).getContext().filename; - std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); - if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { - index = i; - break; - } - } - if (index == (int)~0) { - // Tried to load a parent file that has not been loaded yet. This is bad, - // the launcher should have taken care of this. - std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name - + ", but it has not been loaded yet. Please check your load order."; - esm.fail(fstring); - } - esm.addParentFileIndex(index); - } + // We set the number of plugins here so we can properly verify if valid plugin + // indices are being passed to the LandTexture Store retrieval methods. + mLandTextures.resize(esm.getIndex()+1); // Loop through all records while(esm.hasMoreRecs()) @@ -190,10 +162,10 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) esm.getRecHeader(); // Look up the record type. - std::map::iterator it = mStores.find(n.intval); + std::map::iterator it = mStores.find(n.toInt()); if (it == mStores.end()) { - if (n.intval == ESM::REC_INFO) { + if (n.toInt() == ESM::REC_INFO) { if (dialogue) { dialogue->readInfo(esm, esm.getIndex() != 0); @@ -203,16 +175,23 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) Log(Debug::Error) << "Error: info record without dialog"; esm.skipRecord(); } - } else if (n.intval == ESM::REC_MGEF) { + } else if (n.toInt() == ESM::REC_MGEF) { mMagicEffects.load (esm); - } else if (n.intval == ESM::REC_SKIL) { + } else if (n.toInt() == ESM::REC_SKIL) { mSkills.load (esm); } - else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP) + else if (n.toInt() == ESM::REC_FILT || n.toInt() == ESM::REC_DBGP) { // ignore project file only records esm.skipRecord(); } + else if (n.toInt() == ESM::REC_LUAL) + { + ESM::LuaScriptsCfg cfg; + cfg.load(esm); + // TODO: update refnums in cfg.mScripts[].mInitializationData according to load order + mLuaContent.push_back(std::move(cfg)); + } else { throw std::runtime_error("Unknown record: " + n.toString()); } @@ -224,7 +203,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) continue; } - if (n.intval==ESM::REC_DIAL) { + if (n.toInt() == ESM::REC_DIAL) { dialogue = const_cast(mDialogs.find(id.mId)); } else { dialogue = nullptr; @@ -234,6 +213,32 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } } +ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const +{ + ESM::LuaScriptsCfg cfg; + for (const LuaContent& c : mLuaContent) + { + if (std::holds_alternative(c)) + { + // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. + // It is important for the `reloadlua` console command. + try + { + auto file = std::ifstream(std::get(c)); + std::string fileContent(std::istreambuf_iterator(file), {}); + LuaUtil::parseOMWScripts(cfg, fileContent); + } + catch (std::exception& e) { Log(Debug::Error) << e.what(); } + } + else + { + const ESM::LuaScriptsCfg& addition = std::get(c); + cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); + } + } + return cfg; +} + void ESMStore::setUp(bool validateRecords) { mIds.clear(); @@ -263,12 +268,14 @@ void ESMStore::setUp(bool validateRecords) if (validateRecords) { validate(); - countRecords(); + countAllCellRefs(); } } -void ESMStore::countRecords() +void ESMStore::countAllCellRefs() { + // TODO: We currently need to read entire files here again. + // We should consider consolidating or deferring this reading. if(!mRefCount.empty()) return; std::vector refs; @@ -286,6 +293,7 @@ void ESMStore::countRecords() if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; + // We manually lower case IDs here for the time being to improve performance. Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } @@ -495,9 +503,8 @@ void ESMStore::removeMissingObjects(Store& store) throw std::runtime_error ("Invalid player record (race or class unavailable"); } - std::pair, bool> ESMStore::getSpellList(const std::string& originalId) const + std::pair, bool> ESMStore::getSpellList(const std::string& id) const { - const std::string id = Misc::StringUtils::lowerCase(originalId); auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6ad479f8bf..8582a1daca 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "store.hpp" @@ -74,8 +75,9 @@ namespace MWWorld // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. - std::map mIds; - std::map mStaticIds; + using IDMap = std::unordered_map; + IDMap mIds; + IDMap mStaticIds; std::unordered_map mRefCount; @@ -83,16 +85,25 @@ namespace MWWorld unsigned int mDynamicCount; - mutable std::map > mSpellListCache; + mutable std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> mSpellListCache; /// Validate entries in store after setup void validate(); - void countRecords(); + void countAllCellRefs(); template void removeMissingObjects(Store& store); + + using LuaContent = std::variant< + ESM::LuaScriptsCfg, // data from an omwaddon + std::string>; // path to an omwscripts file + std::vector mLuaContent; + public: + void addOMWScripts(std::string filePath) { mLuaContent.push_back(std::move(filePath)); } + ESM::LuaScriptsCfg getLuaScriptsCfg() const; + /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; @@ -105,10 +116,9 @@ namespace MWWorld } /// Look up the given ID in 'all'. Returns 0 if not found. - /// \note id must be in lower case. int find(const std::string &id) const { - std::map::const_iterator it = mIds.find(id); + IDMap::const_iterator it = mIds.find(id); if (it == mIds.end()) { return 0; } @@ -116,7 +126,7 @@ namespace MWWorld } int findStatic(const std::string &id) const { - std::map::const_iterator it = mStaticIds.find(id); + IDMap::const_iterator it = mStaticIds.find(id); if (it == mStaticIds.end()) { return 0; } diff --git a/apps/openmw/mwworld/groundcoverstore.cpp b/apps/openmw/mwworld/groundcoverstore.cpp new file mode 100644 index 0000000000..543cd3e0d8 --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.cpp @@ -0,0 +1,54 @@ +#include "groundcoverstore.hpp" + +#include +#include + +namespace MWWorld +{ + void GroundcoverStore::init(const Store& statics, const Files::Collections& fileCollections, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder) + { + EsmLoader::Query query; + query.mLoadStatics = true; + query.mLoadCells = true; + + std::vector readers(groundcoverFiles.size()); + const EsmLoader::EsmData content = EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder); + + for (const ESM::Static& stat : statics) + { + std::string id = Misc::StringUtils::lowerCase(stat.mId); + mMeshCache[id] = "meshes\\" + Misc::StringUtils::lowerCase(stat.mModel); + } + + for (const ESM::Static& stat : content.mStatics) + { + std::string id = Misc::StringUtils::lowerCase(stat.mId); + mMeshCache[id] = "meshes\\" + Misc::StringUtils::lowerCase(stat.mModel); + } + + for (const ESM::Cell& cell : content.mCells) + { + if (!cell.isExterior()) continue; + auto cellIndex = std::make_pair(cell.getCellId().mIndex.mX, cell.getCellId().mIndex.mY); + mCellContexts[cellIndex] = std::move(cell.mContextList); + } + } + + std::string GroundcoverStore::getGroundcoverModel(const std::string& id) const + { + std::string idLower = Misc::StringUtils::lowerCase(id); + auto search = mMeshCache.find(idLower); + if (search == mMeshCache.end()) return std::string(); + + return search->second; + } + + void GroundcoverStore::initCell(ESM::Cell& cell, int cellX, int cellY) const + { + cell.blank(); + + auto searchCell = mCellContexts.find(std::make_pair(cellX, cellY)); + if (searchCell != mCellContexts.end()) + cell.mContextList = searchCell->second; + } +} diff --git a/apps/openmw/mwworld/groundcoverstore.hpp b/apps/openmw/mwworld/groundcoverstore.hpp new file mode 100644 index 0000000000..197be2a998 --- /dev/null +++ b/apps/openmw/mwworld/groundcoverstore.hpp @@ -0,0 +1,29 @@ +#ifndef GAME_MWWORLD_GROUNDCOVER_STORE_H +#define GAME_MWWORLD_GROUNDCOVER_STORE_H + +#include +#include +#include + +#include +#include +#include + +#include "esmstore.hpp" + +namespace MWWorld +{ + class GroundcoverStore + { + private: + std::map mMeshCache; + std::map, std::vector> mCellContexts; + + public: + void init(const Store& statics, const Files::Collections& fileCollections, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder); + std::string getGroundcoverModel(const std::string& id) const; + void initCell(ESM::Cell& cell, int cellX, int cellY) const; + }; +} + +#endif diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index 49e60af1fd..8cb0b9b012 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -139,8 +139,8 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) { - const std::string& type = itemPtr.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) + auto type = itemPtr.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(actorPtr); } @@ -431,7 +431,7 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& if (iter.getType() == ContainerStore::Type_Armor) { - if (old.getTypeName() == typeid(ESM::Armor).name()) + if (old.getType() == ESM::Armor::sRecordId) { if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) continue; @@ -465,7 +465,7 @@ void MWWorld::InventoryStore::autoEquipArmor (const MWWorld::Ptr& actor, TSlots& } } - if (old.getTypeName() == typeid(ESM::Clothing).name()) + if (old.getType() == ESM::Clothing::sRecordId) { // check value if (old.getClass().getValue (old) >= test.getClass().getValue (test)) @@ -617,8 +617,8 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { - const std::string& type = item.getTypeName(); - if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) + auto type = item.getType(); + if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(actor); } diff --git a/apps/openmw/mwworld/livecellref.cpp b/apps/openmw/mwworld/livecellref.cpp index 4203e1ac55..62c9f3a2f0 100644 --- a/apps/openmw/mwworld/livecellref.cpp +++ b/apps/openmw/mwworld/livecellref.cpp @@ -11,7 +11,7 @@ #include "class.hpp" #include "esmstore.hpp" -MWWorld::LiveCellRefBase::LiveCellRefBase(const std::string& type, const ESM::CellRef &cref) +MWWorld::LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef &cref) : mClass(&Class::get(type)), mRef(cref), mData(cref) { } @@ -73,3 +73,8 @@ bool MWWorld::LiveCellRefBase::checkStateImp (const ESM::ObjectState& state) { return true; } + +unsigned int MWWorld::LiveCellRefBase::getType() const +{ + return mClass->getType(); +} diff --git a/apps/openmw/mwworld/livecellref.hpp b/apps/openmw/mwworld/livecellref.hpp index 414fde42bd..1ead0395fd 100644 --- a/apps/openmw/mwworld/livecellref.hpp +++ b/apps/openmw/mwworld/livecellref.hpp @@ -1,8 +1,6 @@ #ifndef GAME_MWWORLD_LIVECELLREF_H #define GAME_MWWORLD_LIVECELLREF_H -#include - #include "cellref.hpp" #include "refdata.hpp" @@ -31,7 +29,7 @@ namespace MWWorld /** runtime-data */ RefData mData; - LiveCellRefBase(const std::string& type, const ESM::CellRef &cref=ESM::CellRef()); + LiveCellRefBase(unsigned int type, const ESM::CellRef &cref=ESM::CellRef()); /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase() { } @@ -43,6 +41,11 @@ namespace MWWorld virtual void save (ESM::ObjectState& state) const = 0; ///< Save LiveCellRef state into \a state. + virtual std::string_view getTypeDescription() const = 0; + + unsigned int getType() const; + ///< @see MWWorld::Class::getType + protected: void loadImp (const ESM::ObjectState& state); @@ -77,11 +80,11 @@ namespace MWWorld struct LiveCellRef : public LiveCellRefBase { LiveCellRef(const ESM::CellRef& cref, const X* b = nullptr) - : LiveCellRefBase(typeid(X).name(), cref), mBase(b) + : LiveCellRefBase(X::sRecordId, cref), mBase(b) {} LiveCellRef(const X* b = nullptr) - : LiveCellRefBase(typeid(X).name()), mBase(b) + : LiveCellRefBase(X::sRecordId), mBase(b) {} // The object that this instance is based on. @@ -95,6 +98,8 @@ namespace MWWorld void save (ESM::ObjectState& state) const override; ///< Save LiveCellRef state into \a state. + std::string_view getTypeDescription() const override { return X::getRecordType(); } + static bool checkState (const ESM::ObjectState& state); ///< Check if state is valid and report errors. /// diff --git a/apps/openmw/mwworld/localscripts.cpp b/apps/openmw/mwworld/localscripts.cpp index 1661d6b9f7..0e7dd0771f 100644 --- a/apps/openmw/mwworld/localscripts.cpp +++ b/apps/openmw/mwworld/localscripts.cpp @@ -43,7 +43,7 @@ namespace bool operator()(const MWWorld::Ptr& containerPtr) { // Ignore containers without generated content - if (containerPtr.getTypeName() == typeid(ESM::Container).name() && + if (containerPtr.getType() == ESM::Container::sRecordId && containerPtr.getRefData().getCustomData() == nullptr) return true; diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp new file mode 100644 index 0000000000..7d7e2857fe --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.cpp @@ -0,0 +1,210 @@ +#include "magiceffects.hpp" +#include "esmstore.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/magiceffects.hpp" + +namespace +{ + template + void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName) + { + const T* item = MWBase::Environment::get().getWorld()->getStore().get().search(id); + if(item) + { + enchantment = item->mEnchant; + itemName = item->mName; + } + } +} + +namespace MWWorld +{ + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats) + { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + // Convert corprus to format 10 + for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell) + continue; + + ESM::CreatureStats::CorprusStats stats; + stats.mNextWorsening = oldStats.mNextWorsening; + for (int i=0; imEffects.mList) + { + if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + } + creatureStats.mCorprusSpells[id] = stats; + } + // Convert to format 17 + for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams) + { + const ESM::Spell* spell = store.get().search(id); + if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = spell->mName; + params.mItem.unset(); + params.mCasterActorId = creatureStats.mActorId; + if(spell->mData.mType == ESM::Spell::ST_Ability) + params.mType = ESM::ActiveSpells::Type_Ability; + else + params.mType = ESM::ActiveSpells::Type_Permanent; + params.mWorsenings = -1; + int effectIndex = 0; + for(const auto& enam : spell->mEffects.mList) + { + if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = effectIndex; + auto rand = oldParams.mEffectRands.find(effectIndex); + if(rand != oldParams.mEffectRands.end()) + { + float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + } + else + { + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mFlags = ESM::ActiveEffect::Flag_None; + } + params.mEffects.emplace_back(effect); + } + effectIndex++; + } + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + std::multimap equippedItems; + for(std::size_t i = 0; i < inventory.mItems.size(); ++i) + { + const ESM::ObjectState& item = inventory.mItems[i]; + auto slot = inventory.mEquipmentSlots.find(i); + if(slot != inventory.mEquipmentSlots.end()) + equippedItems.emplace(item.mRef.mRefID, slot->second); + } + for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes) + { + std::string eId; + std::string name; + switch(store.find(id)) + { + case ESM::REC_ARMO: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_CLOT: + getEnchantedItem(id, eId, name); + break; + case ESM::REC_WEAP: + getEnchantedItem(id, eId, name); + break; + } + if(eId.empty()) + continue; + const ESM::Enchantment* enchantment = store.get().search(eId); + if(!enchantment) + continue; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = id; + params.mDisplayName = name; + params.mCasterActorId = creatureStats.mActorId; + params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mWorsenings = -1; + for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + { + const auto& enam = enchantment->mEffects.mList[effectIndex]; + auto [random, multiplier] = oldMagnitudes[effectIndex]; + float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + magnitude *= multiplier; + if(magnitude <= 0) + continue; + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mMagnitude = magnitude; + effect.mMinMagnitude = magnitude; + effect.mMaxMagnitude = magnitude; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effect.mEffectIndex = static_cast(effectIndex); + // Prevent recalculation of resistances and don't reflect or absorb the effect + effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + params.mEffects.emplace_back(effect); + } + auto [begin, end] = equippedItems.equal_range(id); + for(auto it = begin; it != end; ++it) + { + params.mItem = { static_cast(it->second), 0 }; + creatureStats.mActiveSpells.mSpells.emplace_back(params); + } + } + for(const auto& spell : creatureStats.mCorprusSpells) + { + auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; }); + if(it != creatureStats.mActiveSpells.mSpells.end()) + { + it->mNextWorsening = spell.second.mNextWorsening; + int worsenings = 0; + for(int i = 0; i < ESM::Attribute::Length; ++i) + worsenings = std::max(spell.second.mWorsenings[i], worsenings); + it->mWorsenings = worsenings; + } + } + for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap) + { + if(actorId == -1) + continue; + for(auto& params : creatureStats.mActiveSpells.mSpells) + { + if(params.mId == key.mSourceId) + { + bool found = false; + for(auto& effect : params.mEffects) + { + if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex) + { + effect.mArg = actorId; + found = true; + break; + } + } + if(found) + break; + } + } + } + // Reset modifiers that were previously recalculated each frame + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + creatureStats.mAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < 3; ++i) + creatureStats.mDynamic[i].mMod = 0.f; + for(std::size_t i = 0; i < 4; ++i) + creatureStats.mAiSettings[i].mMod = 0.f; + if(npcStats) + { + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + npcStats->mSkills[i].mMod = 0.f; + } + } +} diff --git a/apps/openmw/mwworld/magiceffects.hpp b/apps/openmw/mwworld/magiceffects.hpp new file mode 100644 index 0000000000..31d5ed2038 --- /dev/null +++ b/apps/openmw/mwworld/magiceffects.hpp @@ -0,0 +1,17 @@ +#ifndef OPENMW_MWWORLD_MAGICEFFECTS_H +#define OPENMW_MWWORLD_MAGICEFFECTS_H + +namespace ESM +{ + struct CreatureStats; + struct InventoryState; + struct NpcStats; +} + +namespace MWWorld +{ + void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, + ESM::NpcStats* npcStats = nullptr); +} + +#endif diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index caa0600f7c..4687a4eddd 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -12,6 +12,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/magiceffects.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index 1e4b0ffdf5..a7e42d95e1 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -11,10 +11,10 @@ #include #include +#include namespace ESM { - struct NPC; class ESMWriter; class ESMReader; } diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 9ee137fab6..3bff1854a6 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -22,6 +22,8 @@ #include #include +#include + #include "../mwworld/manualref.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" @@ -407,6 +409,7 @@ namespace MWWorld void ProjectileManager::moveMagicBolts(float duration) { + static const bool normaliseRaceSpeed = Settings::Manager::getBool("normalise race speed", "Game"); for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) @@ -426,13 +429,19 @@ namespace MWWorld } } + const auto& store = MWBase::Environment::get().getWorld()->getStore(); osg::Quat orient = magicBoltState.mNode->getAttitude(); - static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get() - .find("fTargetSpellMaxSpeed")->mValue.getFloat(); + static float fTargetSpellMaxSpeed = store.get().find("fTargetSpellMaxSpeed")->mValue.getFloat(); float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; + if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc()) + { + const auto npc = caster.get()->mBase; + const auto race = store.get().find(npc->mRace); + speed *= npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale; + } osg::Vec3f direction = orient * osg::Vec3f(0,1,0); direction.normalize(); - osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed; + projectile->setVelocity(direction * speed); update(magicBoltState, duration); @@ -441,8 +450,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos); } } @@ -460,7 +467,7 @@ namespace MWWorld // simulating aerodynamics at all projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration; - osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration; + projectile->setVelocity(projectileState.mVelocity); // rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction. if (!projectileState.mThrown) @@ -479,8 +486,6 @@ namespace MWWorld if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); - - mPhysics->updateProjectile(projectileState.mProjectileId, newPos); } } @@ -493,7 +498,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) @@ -529,7 +534,7 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); - const auto pos = projectile->getPosition(); + const auto pos = projectile->getSimulationPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); @@ -546,7 +551,7 @@ namespace MWWorld cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; cast.mSlot = magicBoltState.mSlot; - cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); + cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, true); MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); magicBoltState.mToDelete = true; diff --git a/apps/openmw/mwworld/ptr.cpp b/apps/openmw/mwworld/ptr.cpp deleted file mode 100644 index e16a196297..0000000000 --- a/apps/openmw/mwworld/ptr.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "ptr.hpp" - -#include - -#include "containerstore.hpp" -#include "class.hpp" -#include "livecellref.hpp" - -const std::string& MWWorld::Ptr::getTypeName() const -{ - if(mRef != nullptr) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -MWWorld::LiveCellRefBase *MWWorld::Ptr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -MWWorld::CellRef& MWWorld::Ptr::getCellRef() const -{ - assert(mRef); - - return mRef->mRef; -} - -MWWorld::RefData& MWWorld::Ptr::getRefData() const -{ - assert(mRef); - - return mRef->mData; -} - -void MWWorld::Ptr::setContainerStore (ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -MWWorld::ContainerStore *MWWorld::Ptr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::Ptr::operator const void *() -{ - return mRef; -} - -// ------------------------------------------------------------------------------- - -const std::string &MWWorld::ConstPtr::getTypeName() const -{ - if(mRef != nullptr) - return mRef->mClass->getTypeName(); - throw std::runtime_error("Can't get type name from an empty object."); -} - -const MWWorld::LiveCellRefBase *MWWorld::ConstPtr::getBase() const -{ - if (!mRef) - throw std::runtime_error ("Can't access cell ref pointed to by null Ptr"); - - return mRef; -} - -void MWWorld::ConstPtr::setContainerStore (const ContainerStore *store) -{ - assert (store); - assert (!mCell); - - mContainerStore = store; -} - -const MWWorld::ContainerStore *MWWorld::ConstPtr::getContainerStore() const -{ - return mContainerStore; -} - -MWWorld::ConstPtr::operator const void *() -{ - return mRef; -} diff --git a/apps/openmw/mwworld/ptr.hpp b/apps/openmw/mwworld/ptr.hpp index 9ab18d7f48..4dbdfa5545 100644 --- a/apps/openmw/mwworld/ptr.hpp +++ b/apps/openmw/mwworld/ptr.hpp @@ -2,8 +2,9 @@ #define GAME_MWWORLD_PTR_H #include - +#include #include +#include #include #include "livecellref.hpp" @@ -15,27 +16,41 @@ namespace MWWorld struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef - - class Ptr + /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr + template class TypeTransform> + class PtrBase { public: - MWWorld::LiveCellRefBase *mRef; - CellStore *mCell; - ContainerStore *mContainerStore; + typedef TypeTransform LiveCellRefBaseType; + typedef TypeTransform CellStoreType; + typedef TypeTransform ContainerStoreType; - public: - Ptr(MWWorld::LiveCellRefBase *liveCellRef=nullptr, CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } + LiveCellRefBaseType *mRef; + CellStoreType *mCell; + ContainerStoreType *mContainerStore; bool isEmpty() const { return mRef == nullptr; } - const std::string& getTypeName() const; + // Returns a 32-bit id of the ESM record this object is based on. + // Specific values of ids are defined in ESM::RecNameInts. + // Note 1: ids are not sequential. E.g. for a creature `getType` returns 0x41455243. + // Note 2: Life is not easy and full of surprises. For example + // prison marker reuses ESM::Door record. Player is ESM::NPC. + unsigned int getType() const + { + if(mRef != nullptr) + return mRef->getType(); + throw std::runtime_error("Can't get type name from an empty object."); + } + + std::string_view getTypeDescription() const + { + return mRef ? mRef->getTypeDescription() : "nullptr"; + } const Class& getClass() const { @@ -45,26 +60,39 @@ namespace MWWorld } template - MWWorld::LiveCellRef *get() const + TypeTransform> *get() const { - MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); + TypeTransform> *ref = dynamic_cast>*>(mRef); if(ref) return ref; std::stringstream str; - str<< "Bad LiveCellRef cast to "<& getCellRef() const + { + assert(mRef); + return mRef->mRef; + } - RefData& getRefData() const; + TypeTransform& getRefData() const + { + assert(mRef); + return mRef->mData; + } - CellStore *getCell() const + CellStoreType *getCell() const { assert(mCell); return mCell; @@ -75,159 +103,47 @@ namespace MWWorld return (mContainerStore == nullptr) && (mCell != nullptr); } - void setContainerStore (ContainerStore *store); + void setContainerStore (ContainerStoreType *store) ///< Must not be called on references that are in a cell. + { + assert (store); + assert (!mCell); + mContainerStore = store; + } - ContainerStore *getContainerStore() const; + ContainerStoreType *getContainerStore() const ///< May return a 0-pointer, if reference is not in a container. + { + return mContainerStore; + } - operator const void *(); + operator const void *() const ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + { + return mRef; + } + + protected: + PtrBase(LiveCellRefBaseType *liveCellRef, CellStoreType *cell, ContainerStoreType* containerStore) : mRef(liveCellRef), mCell(cell), mContainerStore(containerStore) {} }; - /// \brief Pointer to a const LiveCellRef - /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. - class ConstPtr + /// @note It is possible to get mutable values from const Ptr. So if a function accepts const Ptr&, the object is still mutable. + /// To make it really const the argument should be const ConstPtr&. + class Ptr : public PtrBase { public: - - const MWWorld::LiveCellRefBase *mRef; - const CellStore *mCell; - const ContainerStore *mContainerStore; - - public: - ConstPtr(const MWWorld::LiveCellRefBase *liveCellRef=nullptr, const CellStore *cell=nullptr) - : mRef(liveCellRef), mCell(cell), mContainerStore(nullptr) - { - } - - ConstPtr(const MWWorld::Ptr& ptr) - : mRef(ptr.mRef), mCell(ptr.mCell), mContainerStore(ptr.mContainerStore) - { - } - - bool isEmpty() const - { - return mRef == nullptr; - } - - const std::string& getTypeName() const; - - const Class& getClass() const - { - if(mRef != nullptr) - return *(mRef->mClass); - throw std::runtime_error("Cannot get class of an empty object"); - } - - template - const MWWorld::LiveCellRef *get() const - { - const MWWorld::LiveCellRef *ref = dynamic_cast*>(mRef); - if(ref) return ref; - - std::stringstream str; - str<< "Bad LiveCellRef cast to "<mRef; - } - - const RefData& getRefData() const - { - assert(mRef); - return mRef->mData; - } - - const CellStore *getCell() const - { - assert(mCell); - return mCell; - } - - bool isInCell() const - { - return (mContainerStore == nullptr) && (mCell != nullptr); - } - - void setContainerStore (const ContainerStore *store); - ///< Must not be called on references that are in a cell. - - const ContainerStore *getContainerStore() const; - ///< May return a 0-pointer, if reference is not in a container. - - operator const void *(); - ///< Return a 0-pointer, if Ptr is empty; return a non-0-pointer, if Ptr is not empty + Ptr(LiveCellRefBase *liveCellRef=nullptr, CellStoreType *cell=nullptr) : PtrBase(liveCellRef, cell, nullptr) {} }; - inline bool operator== (const Ptr& left, const Ptr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const Ptr& left, const Ptr& right) - { - return !(left==right); - } - - inline bool operator< (const Ptr& left, const Ptr& right) - { - return left.mRef= (const Ptr& left, const Ptr& right) - { - return !(left (const Ptr& left, const Ptr& right) - { - return rightright); - } - - inline bool operator== (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef==right.mRef; - } - - inline bool operator!= (const ConstPtr& left, const ConstPtr& right) - { - return !(left==right); - } - - inline bool operator< (const ConstPtr& left, const ConstPtr& right) - { - return left.mRef= (const ConstPtr& left, const ConstPtr& right) - { - return !(left (const ConstPtr& left, const ConstPtr& right) + /// @note The difference between Ptr and ConstPtr is that the second one adds const to the underlying pointers. + /// @note a Ptr can be implicitely converted to a ConstPtr, but you can not convert a ConstPtr to a Ptr. + class ConstPtr : public PtrBase { - return rightright); - } } #endif diff --git a/apps/openmw/mwworld/recordcmp.hpp b/apps/openmw/mwworld/recordcmp.hpp deleted file mode 100644 index f749351cea..0000000000 --- a/apps/openmw/mwworld/recordcmp.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef OPENMW_MWWORLD_RECORDCMP_H -#define OPENMW_MWWORLD_RECORDCMP_H - -#include - -#include - -namespace MWWorld -{ - struct RecordCmp - { - template - bool operator()(const T &x, const T& y) const { - return x.mId < y.mId; - } - }; - - template <> - inline bool RecordCmp::operator()(const ESM::Dialogue &x, const ESM::Dialogue &y) const { - return Misc::StringUtils::ciLess(x.mId, y.mId); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Cell &x, const ESM::Cell &y) const { - return Misc::StringUtils::ciLess(x.mName, y.mName); - } - - template <> - inline bool RecordCmp::operator()(const ESM::Pathgrid &x, const ESM::Pathgrid &y) const { - return Misc::StringUtils::ciLess(x.mCell, y.mCell); - } - -} // end namespace -#endif diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index ed68a46059..15b3477cb8 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -65,23 +64,12 @@ namespace * osg::Quat(zr, osg::Vec3(0, 0, -1)); } - osg::Quat makeObjectOsgQuat(const ESM::Position& position) - { - const float xr = position.rot[0]; - const float yr = position.rot[1]; - const float zr = position.rot[2]; - - return osg::Quat(zr, osg::Vec3(0, 0, -1)) - * osg::Quat(yr, osg::Vec3(0, -1, 0)) - * osg::Quat(xr, osg::Vec3(-1, 0, 0)); - } - osg::Quat makeNodeRotation(const MWWorld::Ptr& ptr, RotationOrder order) { const auto pos = ptr.getRefData().getPosition(); const auto rot = ptr.getClass().isActor() ? makeActorOsgQuat(pos) - : (order == RotationOrder::inverse ? makeInversedOrderObjectOsgQuat(pos) : makeObjectOsgQuat(pos)); + : (order == RotationOrder::inverse ? makeInversedOrderObjectOsgQuat(pos) : Misc::Convert::makeOsgQuat(pos)); return rot; } @@ -144,7 +132,7 @@ namespace { btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; @@ -155,16 +143,16 @@ namespace const auto transform = object->getTransform(); const btTransform closedDoorTransform( - Misc::Convert::toBullet(makeObjectOsgQuat(ptr.getCellRef().getPosition())), + Misc::Convert::makeBulletQuaternion(ptr.getCellRef().getPosition()), transform.getOrigin() ); - const auto start = Misc::Convert::makeOsgVec3f(closedDoorTransform(center + toPoint)); + const auto start = Misc::Convert::toOsg(closedDoorTransform(center + toPoint)); const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start; - const auto end = Misc::Convert::makeOsgVec3f(closedDoorTransform(center - toPoint)); + const auto end = Misc::Convert::toOsg(closedDoorTransform(center - toPoint)); const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {}, MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water); const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end; @@ -383,17 +371,17 @@ namespace MWWorld { osg::ref_ptr land = mRendering.getLandManager()->getLand(cellX, cellY); const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : nullptr; - const float verts = ESM::Land::LAND_SIZE; - const float worldsize = ESM::Land::REAL_SIZE; + const int verts = ESM::Land::LAND_SIZE; + const int worldsize = ESM::Land::REAL_SIZE; if (data) { - mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); + mPhysics->addHeightField(data->mHeights, cellX, cellY, worldsize, verts, data->mMinHeight, data->mMaxHeight, land.get()); } else { static std::vector defaultHeight; defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT); - mPhysics->addHeightField (&defaultHeight[0], cellX, cellY, worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); + mPhysics->addHeightField(defaultHeight.data(), cellX, cellY, worldsize, verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get()); } if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) { @@ -416,7 +404,7 @@ namespace MWWorld return heights; } } (); - mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shift, shape); + mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shape); } } @@ -446,18 +434,11 @@ namespace MWWorld if (cell->getCell()->isExterior()) { if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - { - const btTransform& transform =heightField->getCollisionObject()->getWorldTransform(); - mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, - osg::Vec3f(static_cast(transform.getOrigin().x()), - static_cast(transform.getOrigin().y()), - waterLevel)); - } + mNavigator.addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, waterLevel); } else { - mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), - osg::Vec3f(0, 0, waterLevel)); + mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), waterLevel); } } else @@ -665,7 +646,6 @@ namespace MWWorld } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); - mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; @@ -712,7 +692,6 @@ namespace MWWorld } mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime()); - mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue()); loadingListener->increaseProgress (1); i++; @@ -766,9 +745,6 @@ namespace MWWorld mPreloader.reset(new CellPreloader(rendering.getResourceSystem(), physics->getShapeManager(), rendering.getTerrain(), rendering.getLandManager())); mPreloader->setWorkQueue(mRendering.getWorkQueue()); - mPreloader->setUnrefQueue(rendering.getUnrefQueue()); - mPhysics->setUnrefQueue(rendering.getUnrefQueue()); - rendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells")); mPreloader->setExpiryDelay(Settings::Manager::getFloat("preload cell expiry delay", "Cells")); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 718cebd790..4d720a11a7 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -38,8 +38,8 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - - mStatic.insert_or_assign(record.mIndex, record); + auto idx = record.mIndex; + mStatic.insert_or_assign(idx, std::move(record)); } template int IndexedStore::getSize() const @@ -64,8 +64,9 @@ namespace MWWorld const T *ptr = search(index); if (ptr == nullptr) { - const std::string msg = T::getRecordType() + " with index " + std::to_string(index) + " not found"; - throw std::runtime_error(msg); + std::stringstream msg; + msg << T::getRecordType() << " with index " << index << " not found"; + throw std::runtime_error(msg.str()); } return ptr; } @@ -97,13 +98,11 @@ namespace MWWorld template const T *Store::search(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename Dynamic::const_iterator dit = mDynamic.find(idLower); + typename Dynamic::const_iterator dit = mDynamic.find(id); if (dit != mDynamic.end()) return &dit->second; - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -112,8 +111,7 @@ namespace MWWorld template const T *Store::searchStatic(const std::string &id) const { - std::string idLower = Misc::StringUtils::lowerCase(id); - typename std::map::const_iterator it = mStatic.find(idLower); + typename Static::const_iterator it = mStatic.find(id); if (it != mStatic.end()) return &(it->second); @@ -145,8 +143,9 @@ namespace MWWorld const T *ptr = search(id); if (ptr == nullptr) { - const std::string msg = T::getRecordType() + " '" + id + "' not found"; - throw std::runtime_error(msg); + std::stringstream msg; + msg << T::getRecordType() << " '" << id << "' not found"; + throw std::runtime_error(msg.str()); } return ptr; } @@ -157,7 +156,7 @@ namespace MWWorld bool isDeleted = false; record.load(esm, isDeleted); - Misc::StringUtils::lowerCaseInPlace(record.mId); + Misc::StringUtils::lowerCaseInPlace(record.mId); // TODO: remove this line once we have ported our remaining code base to lowercase on lookup std::pair inserted = mStatic.insert_or_assign(record.mId, record); if (inserted.second) @@ -204,14 +203,13 @@ namespace MWWorld template T *Store::insert(const T &item, bool overrideOnly) { - std::string id = Misc::StringUtils::lowerCase(item.mId); if(overrideOnly) { - auto it = mStatic.find(id); + auto it = mStatic.find(item.mId); if(it == mStatic.end()) return nullptr; } - std::pair result = mDynamic.insert_or_assign(id, item); + std::pair result = mDynamic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -220,8 +218,7 @@ namespace MWWorld template T *Store::insertStatic(const T &item) { - std::string id = Misc::StringUtils::lowerCase(item.mId); - std::pair result = mStatic.insert_or_assign(id, item); + std::pair result = mStatic.insert_or_assign(item.mId, item); T *ptr = &result.first->second; if (result.second) mShared.push_back(ptr); @@ -230,9 +227,7 @@ namespace MWWorld template bool Store::eraseStatic(const std::string &id) { - std::string idLower = Misc::StringUtils::lowerCase(id); - - typename std::map::iterator it = mStatic.find(idLower); + typename Static::iterator it = mStatic.find(id); if (it != mStatic.end()) { // delete from the static part of mShared @@ -240,7 +235,7 @@ namespace MWWorld typename std::vector::iterator end = sharedIter + mStatic.size(); while (sharedIter != mShared.end() && sharedIter != end) { - if((*sharedIter)->mId == idLower) { + if(Misc::StringUtils::ciEqual((*sharedIter)->mId, id)) { mShared.erase(sharedIter); break; } @@ -255,17 +250,13 @@ namespace MWWorld template bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - typename Dynamic::iterator it = mDynamic.find(key); - if (it == mDynamic.end()) { + if (!mDynamic.erase(id)) return false; - } - mDynamic.erase(it); // have to reinit the whole shared part assert(mShared.size() >= mStatic.size()); mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); - for (it = mDynamic.begin(); it != mDynamic.end(); ++it) { + for (auto it = mDynamic.begin(); it != mDynamic.end(); ++it) { mShared.push_back(&it->second); } return true; @@ -302,11 +293,6 @@ namespace MWWorld //========================================================================= Store::Store() { - mStatic.emplace_back(); - LandTextureList <exl = mStatic[0]; - // More than enough to hold Morrowind.esm. Extra lists for plugins will we - // added on-the-fly in a different method. - ltexl.reserve(128); } const ESM::LandTexture *Store::search(size_t index, size_t plugin) const { @@ -336,42 +322,33 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].size(); } - RecordId Store::load(ESM::ESMReader &esm, size_t plugin) + RecordId Store::load(ESM::ESMReader &esm) { ESM::LandTexture lt; bool isDeleted = false; lt.load(esm, isDeleted); - assert(plugin < mStatic.size()); - // Replace texture for records with given ID and index from all plugins. for (unsigned int i=0; i(search(lt.mIndex, i)); if (tex) { - const std::string texId = Misc::StringUtils::lowerCase(tex->mId); - const std::string ltId = Misc::StringUtils::lowerCase(lt.mId); - if (texId == ltId) - { + if (Misc::StringUtils::ciEqual(tex->mId, lt.mId)) tex->mTexture = lt.mTexture; - } } } - LandTextureList <exl = mStatic[plugin]; + LandTextureList <exl = mStatic.back(); if(lt.mIndex + 1 > (int)ltexl.size()) ltexl.resize(lt.mIndex+1); // Store it - ltexl[lt.mIndex] = lt; + auto idx = lt.mIndex; + ltexl[idx] = std::move(lt); - return RecordId(lt.mId, isDeleted); - } - RecordId Store::load(ESM::ESMReader &esm) - { - return load(esm, esm.getIndex()); + return RecordId(ltexl[idx].mId, isDeleted); } Store::iterator Store::begin(size_t plugin) const { @@ -383,11 +360,6 @@ namespace MWWorld assert(plugin < mStatic.size()); return mStatic[plugin].end(); } - void Store::resize(size_t num) - { - if (mStatic.size() < num) - mStatic.resize(num); - } // Land //========================================================================= @@ -501,16 +473,12 @@ namespace MWWorld } const ESM::Cell *Store::search(const std::string &id) const { - ESM::Cell cell; - cell.mName = Misc::StringUtils::lowerCase(id); - - std::map::const_iterator it = mInt.find(cell.mName); - + DynamicInt::const_iterator it = mInt.find(id); if (it != mInt.end()) { return &(it->second); } - DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); + DynamicInt::const_iterator dit = mDynamicInt.find(id); if (dit != mDynamicInt.end()) { return &dit->second; } @@ -519,48 +487,34 @@ namespace MWWorld } const ESM::Cell *Store::search(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } return nullptr; } const ESM::Cell *Store::searchStatic(int x, int y) const { - ESM::Cell cell; - cell.mData.mX = x; - cell.mData.mY = y; - - std::pair key(x, y); - DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + DynamicExt::const_iterator it = mExt.find(std::make_pair(x,y)); + if (it != mExt.end()) return &(it->second); - } return nullptr; } const ESM::Cell *Store::searchOrCreate(int x, int y) { std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); - if (it != mExt.end()) { + if (it != mExt.end()) return &(it->second); - } DynamicExt::const_iterator dit = mDynamicExt.find(key); - if (dit != mDynamicExt.end()) { + if (dit != mDynamicExt.end()) return &dit->second; - } ESM::Cell newCell; newCell.mData.mX = x; @@ -623,12 +577,11 @@ namespace MWWorld // Load the (x,y) coordinates of the cell, if it is an exterior cell, // so we can find the cell we need to merge with cell.loadNameAndData(esm, isDeleted); - std::string idLower = Misc::StringUtils::lowerCase(cell.mName); if(cell.mData.mFlags & ESM::Cell::Interior) { // Store interior cell by name, try to merge with existing parent data. - ESM::Cell *oldcell = const_cast(search(idLower)); + ESM::Cell *oldcell = const_cast(search(cell.mName)); if (oldcell) { // merge new cell into old cell // push the new references on the list of references to manage (saveContext = true) @@ -640,7 +593,7 @@ namespace MWWorld // spawn a new cell cell.loadCell(esm, true); - mInt[idLower] = cell; + mInt[cell.mName] = cell; } } else @@ -778,27 +731,19 @@ namespace MWWorld const std::string cellType = (cell.isExterior()) ? "exterior" : "interior"; throw std::runtime_error("Failed to create " + cellType + " cell"); } - ESM::Cell *ptr; if (cell.isExterior()) { std::pair key(cell.getGridX(), cell.getGridY()); // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicExt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedExt.push_back(ptr); + DynamicExt::iterator result = mDynamicExt.emplace(key, cell).first; + mSharedExt.push_back(&result->second); + return &result->second; } else { - std::string key = Misc::StringUtils::lowerCase(cell.mName); - // duplicate insertions are avoided by search(ESM::Cell &) - std::pair result = - mDynamicInt.insert(std::make_pair(key, cell)); - - ptr = &result.first->second; - mSharedInt.push_back(ptr); + DynamicInt::iterator result = mDynamicInt.emplace(cell.mName, cell).first; + mSharedInt.push_back(&result->second); + return &result->second; } - return ptr; } bool Store::erase(const ESM::Cell &cell) { @@ -809,8 +754,7 @@ namespace MWWorld } bool Store::erase(const std::string &id) { - std::string key = Misc::StringUtils::lowerCase(id); - DynamicInt::iterator it = mDynamicInt.find(key); + DynamicInt::iterator it = mDynamicInt.find(id); if (it == mDynamicInt.end()) { return false; @@ -1050,6 +994,9 @@ namespace MWWorld mShared.reserve(mStatic.size()); for (auto & [_, dial] : mStatic) mShared.push_back(&dial); + // TODO: verify and document this inconsistent behaviour + // TODO: if we require this behaviour, maybe we should move it to the place that requires it + std::sort(mShared.begin(), mShared.end(), [](const ESM::Dialogue* l, const ESM::Dialogue* r) -> bool { return l->mId < r->mId; }); } template <> @@ -1060,12 +1007,11 @@ namespace MWWorld dialogue.loadId(esm); - std::string idLower = Misc::StringUtils::lowerCase(dialogue.mId); - std::map::iterator found = mStatic.find(idLower); + Static::iterator found = mStatic.find(dialogue.mId); if (found == mStatic.end()) { dialogue.loadData(esm, isDeleted); - mStatic.insert(std::make_pair(idLower, dialogue)); + mStatic.emplace(dialogue.mId, dialogue); } else { @@ -1079,11 +1025,7 @@ namespace MWWorld template<> bool Store::eraseStatic(const std::string &id) { - auto it = mStatic.find(Misc::StringUtils::lowerCase(id)); - - if (it != mStatic.end()) - mStatic.erase(it); - + mStatic.erase(id); return true; } diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index 4b1d648703..22f36da690 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -5,9 +5,11 @@ #include #include #include +#include #include -#include "recordcmp.hpp" +#include +#include namespace ESM { @@ -147,14 +149,15 @@ namespace MWWorld template class Store : public StoreBase { - std::map mStatic; - std::vector mShared; // Preserves the record order as it came from the content files (this - // is relevant for the spell autocalc code and selection order - // for heads/hairs in the character creation) - std::map mDynamic; - - typedef std::map Dynamic; - typedef std::map Static; + typedef std::unordered_map Static; + Static mStatic; + /// @par mShared usually preserves the record order as it came from the content files (this + /// is relevant for the spell autocalc code and selection order + /// for heads/hairs in the character creation) + /// @warning ESM::Dialogue Store currently implements a sorted order for unknown reasons. + std::vector mShared; + typedef std::unordered_map Dynamic; + Dynamic mDynamic; friend class ESMStore; @@ -219,13 +222,11 @@ namespace MWWorld const ESM::LandTexture *search(size_t index, size_t plugin) const; const ESM::LandTexture *find(size_t index, size_t plugin) const; - /// Resize the internal store to hold at least \a num plugins. - void resize(size_t num); + void resize(size_t num) { mStatic.resize(num); } size_t getSize() const override; size_t getSize(size_t plugin) const; - RecordId load(ESM::ESMReader &esm, size_t plugin); RecordId load(ESM::ESMReader &esm) override; iterator begin(size_t plugin) const; @@ -294,7 +295,7 @@ namespace MWWorld } }; - typedef std::map DynamicInt; + typedef std::unordered_map DynamicInt; typedef std::map, ESM::Cell, DynamicExtCmp> DynamicExt; DynamicInt mInt; @@ -354,7 +355,7 @@ namespace MWWorld class Store : public StoreBase { private: - typedef std::map Interior; + typedef std::unordered_map Interior; typedef std::map, ESM::Pathgrid> Exterior; Interior mInt; diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 4bdd784db1..965c690238 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -22,8 +22,6 @@ #include -using namespace MWWorld; - namespace { static const int invalidWeatherID = -1; @@ -38,1226 +36,1240 @@ namespace { return x * (1-factor) + y * factor; } -} -template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const -{ - WeatherSetting setting = timeSettings.getSetting(prefix); - float preSunriseTime = setting.mPreSunriseTime; - float postSunriseTime = setting.mPostSunriseTime; - float preSunsetTime = setting.mPreSunsetTime; - float postSunsetTime = setting.mPostSunsetTime; - - // night - if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) - return mNightValue; - // sunrise - else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) + osg::Vec3f calculateStormDirection(const std::string& particleEffect) { - float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; - float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; - - if (gameHour <= middle) - { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mNightValue, factor); - } - else + osg::Vec3f stormDirection = MWWorld::Weather::defaultDirection(); + if (particleEffect == "meshes\\ashcloud.nif" || particleEffect == "meshes\\blightcloud.nif") { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunriseValue, mDayValue, factor); + osg::Vec3f playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3(); + playerPos.z() = 0; + osg::Vec3f redMountainPos = osg::Vec3f(25000.f, 70000.f, 0.f); + stormDirection = (playerPos - redMountainPos); + stormDirection.normalize(); } + return stormDirection; } - // day - else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) - return mDayValue; - // sunset - else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) - { - float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; - float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; +} - if (gameHour <= middle) +namespace MWWorld +{ + template + T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const + { + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; + + // night + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) + return mNightValue; + // sunrise + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - // fade in - float advance = middle - gameHour; - float factor = 0.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mDayValue, factor); + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mNightValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunriseValue, mDayValue, factor); + } } - else + // day + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) + return mDayValue; + // sunset + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { - // fade out - float advance = gameHour - middle; - float factor = 1.f; - if (duration > 0) - factor = advance / duration * 2; - return lerp(mSunsetValue, mNightValue, factor); + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) + { + // fade in + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mDayValue, factor); + } + else + { + // fade out + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; + return lerp(mSunsetValue, mNightValue, factor); + } } + // shut up compiler + return T(); } - // shut up compiler - return T(); -} - - -template class MWWorld::TimeOfDayInterpolator; -template class MWWorld::TimeOfDayInterpolator; - -Weather::Weather(const std::string& name, - float stormWindSpeed, - float rainSpeed, - float dlFactor, - float dlOffset, - const std::string& particleEffect) - : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) - , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) - , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) - , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) - , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), - Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) - , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), - Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) - , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) - , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) - , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) - , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) - , mIsStorm(mWindSpeed > stormWindSpeed) - , mRainSpeed(rainSpeed) - , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) - , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) - , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) - , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) - , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) - , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) - , mParticleEffect(particleEffect) - , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") - , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) - , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) - , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) - , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) - , mThunderSoundID() - , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) - , mFlashBrightness(0.0f) -{ - mDL.FogFactor = dlFactor; - mDL.FogOffset = dlOffset; - mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); - mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); - mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); - mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); - - // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. + template class MWWorld::TimeOfDayInterpolator; + template class MWWorld::TimeOfDayInterpolator; - if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect + osg::Vec3f Weather::defaultDirection() { - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); - if (mAmbientLoopSoundID.empty()) // default to "rain" if not set - mAmbientLoopSoundID = "rain"; + static const osg::Vec3f direction = osg::Vec3f(0.f, 1.f, 0.f); + return direction; } - else - mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); - if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) - mAmbientLoopSoundID.clear(); -} + Weather::Weather(const std::string& name, + float stormWindSpeed, + float rainSpeed, + float dlFactor, + float dlOffset, + const std::string& particleEffect) + : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) + , mSkyColor(Fallback::Map::getColour("Weather_" + name +"_Sky_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sky_Night_Color")) + , mFogColor(Fallback::Map::getColour("Weather_" + name + "_Fog_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Fog_Night_Color")) + , mAmbientColor(Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Ambient_Night_Color")) + , mSunColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Sunrise_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Day_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Sunset_Color"), + Fallback::Map::getColour("Weather_" + name + "_Sun_Night_Color")) + , mLandFogDepth(Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Day_Depth"), + Fallback::Map::getFloat("Weather_" + name + "_Land_Fog_Night_Depth")) + , mSunDiscSunsetColor(Fallback::Map::getColour("Weather_" + name + "_Sun_Disc_Sunset_Color")) + , mWindSpeed(Fallback::Map::getFloat("Weather_" + name + "_Wind_Speed")) + , mCloudSpeed(Fallback::Map::getFloat("Weather_" + name + "_Cloud_Speed")) + , mGlareView(Fallback::Map::getFloat("Weather_" + name + "_Glare_View")) + , mIsStorm(mWindSpeed > stormWindSpeed) + , mRainSpeed(rainSpeed) + , mRainEntranceSpeed(Fallback::Map::getFloat("Weather_" + name + "_Rain_Entrance_Speed")) + , mRainMaxRaindrops(Fallback::Map::getFloat("Weather_" + name + "_Max_Raindrops")) + , mRainDiameter(Fallback::Map::getFloat("Weather_" + name + "_Rain_Diameter")) + , mRainThreshold(Fallback::Map::getFloat("Weather_" + name + "_Rain_Threshold")) + , mRainMinHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Min")) + , mRainMaxHeight(Fallback::Map::getFloat("Weather_" + name + "_Rain_Height_Max")) + , mParticleEffect(particleEffect) + , mRainEffect(Fallback::Map::getBool("Weather_" + name + "_Using_Precip") ? "meshes\\raindrop.nif" : "") + , mStormDirection(Weather::defaultDirection()) + , mTransitionDelta(Fallback::Map::getFloat("Weather_" + name + "_Transition_Delta")) + , mCloudsMaximumPercent(Fallback::Map::getFloat("Weather_" + name + "_Clouds_Maximum_Percent")) + , mThunderFrequency(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Frequency")) + , mThunderThreshold(Fallback::Map::getFloat("Weather_" + name + "_Thunder_Threshold")) + , mThunderSoundID() + , mFlashDecrement(Fallback::Map::getFloat("Weather_" + name + "_Flash_Decrement")) + , mFlashBrightness(0.0f) + { + mDL.FogFactor = dlFactor; + mDL.FogOffset = dlOffset; + mThunderSoundID[0] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_0"); + mThunderSoundID[1] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_1"); + mThunderSoundID[2] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_2"); + mThunderSoundID[3] = Fallback::Map::getString("Weather_" + name + "_Thunder_Sound_ID_3"); -float Weather::transitionDelta() const -{ - // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the - // measurement is in real time, not in-game time. - return mTransitionDelta; -} + // TODO: support weathers that have both "Ambient Loop Sound ID" and "Rain Loop Sound ID", need to play both sounds at the same time. -float Weather::cloudBlendFactor(const float transitionRatio) const -{ - // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. - return transitionRatio / mCloudsMaximumPercent; -} - -float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) -{ - // When paused, the flash brightness remains the same and no new strikes can occur. - if(!isPaused) - { - // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. - if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + if (!mRainEffect.empty()) // NOTE: in vanilla, the weathers with rain seem to be hardcoded; changing Using_Precip has no effect { - flashDecrement(elapsedSeconds); - - if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) - { - lightningAndThunder(); - } + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Rain_Loop_Sound_ID"); + if (mAmbientLoopSoundID.empty()) // default to "rain" if not set + mAmbientLoopSoundID = "rain"; } else - { - mFlashBrightness = 0.0f; - } - } + mAmbientLoopSoundID = Fallback::Map::getString("Weather_" + name + "_Ambient_Loop_Sound_ID"); - return mFlashBrightness; -} - -inline void Weather::flashDecrement(const float elapsedSeconds) -{ - // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was - // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). - float decrement = mFlashDecrement * elapsedSeconds; - mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; -} - -inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const -{ - // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes - // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of - // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to - // scaled based on how far past it is past the Thunder Threshold. - float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); - return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; -} - -inline void Weather::lightningAndThunder(void) -{ - // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. - // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance - // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. - // TODO: Determine the distribution of each distance to see if it's evenly weighted. - unsigned int distance = Misc::Rng::rollDice(4); - // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. - mFlashBrightness += 1 - (distance * 0.25f); - MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); -} + if (Misc::StringUtils::ciEqual(mAmbientLoopSoundID, "None")) + mAmbientLoopSoundID.clear(); + } -RegionWeather::RegionWeather(const ESM::Region& region) - : mWeather(invalidWeatherID) - , mChances() -{ - mChances.reserve(10); - mChances.push_back(region.mData.mClear); - mChances.push_back(region.mData.mCloudy); - mChances.push_back(region.mData.mFoggy); - mChances.push_back(region.mData.mOvercast); - mChances.push_back(region.mData.mRain); - mChances.push_back(region.mData.mThunder); - mChances.push_back(region.mData.mAsh); - mChances.push_back(region.mData.mBlight); - mChances.push_back(region.mData.mA); - mChances.push_back(region.mData.mB); -} + float Weather::transitionDelta() const + { + // Transition Delta describes how quickly transitioning to the weather in question will take, in Hz. Note that the + // measurement is in real time, not in-game time. + return mTransitionDelta; + } -RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) - : mWeather(state.mWeather) - , mChances(state.mChances) -{ -} + float Weather::cloudBlendFactor(const float transitionRatio) const + { + // Clouds Maximum Percent affects how quickly the sky transitions from one sky texture to the next. + return transitionRatio / mCloudsMaximumPercent; + } -RegionWeather::operator ESM::RegionWeatherState() const -{ - ESM::RegionWeatherState state = + float Weather::calculateThunder(const float transitionRatio, const float elapsedSeconds, const bool isPaused) + { + // When paused, the flash brightness remains the same and no new strikes can occur. + if(!isPaused) { - mWeather, - mChances - }; + // Morrowind doesn't appear to do any calculations unless the transition ratio is higher than the Thunder Threshold. + if(transitionRatio >= mThunderThreshold && mThunderFrequency > 0.0f) + { + flashDecrement(elapsedSeconds); - return state; -} + if(Misc::Rng::rollProbability() <= thunderChance(transitionRatio, elapsedSeconds)) + { + lightningAndThunder(); + } + } + else + { + mFlashBrightness = 0.0f; + } + } -void RegionWeather::setChances(const std::vector& chances) -{ - if(mChances.size() < chances.size()) + return mFlashBrightness; + } + + inline void Weather::flashDecrement(const float elapsedSeconds) { - mChances.reserve(chances.size()); + // The Flash Decrement is measured in whole units per second. This means that if the flash brightness was + // currently 1.0, then it should take approximately 0.25 seconds to decay to 0.0 (the minimum). + float decrement = mFlashDecrement * elapsedSeconds; + mFlashBrightness = decrement > mFlashBrightness ? 0.0f : mFlashBrightness - decrement; } - int i = 0; - for(char chance : chances) + inline float Weather::thunderChance(const float transitionRatio, const float elapsedSeconds) const { - mChances[i] = chance; - i++; + // This formula is reversed from the observation that with Thunder Frequency set to 1, there are roughly 10 strikes + // per minute. It doesn't appear to be tied to in game time as Timescale doesn't affect it. Various values of + // Thunder Frequency seem to change the average number of strikes in a linear fashion.. During a transition, it appears to + // scaled based on how far past it is past the Thunder Threshold. + float scaleFactor = (transitionRatio - mThunderThreshold) / (1.0f - mThunderThreshold); + return ((mThunderFrequency * 10.0f) / 60.0f) * elapsedSeconds * scaleFactor; } - // Regional weather no longer supports the current type, select a new weather pattern. - if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) + inline void Weather::lightningAndThunder(void) { - chooseNewWeather(); + // Morrowind seems to vary the intensity of the brightness based on which of the four sound IDs it selects. + // They appear to go from 0 (brightest, closest) to 3 (faintest, farthest). The value of 0.25 per distance + // was derived by setting the Flash Decrement to 0.1 and measuring how long each value took to decay to 0. + // TODO: Determine the distribution of each distance to see if it's evenly weighted. + unsigned int distance = Misc::Rng::rollDice(4); + // Flash brightness appears additive, since if multiple strikes occur, it takes longer for it to decay to 0. + mFlashBrightness += 1 - (distance * 0.25f); + MWBase::Environment::get().getSoundManager()->playSound(mThunderSoundID[distance], 1.0, 1.0); } -} -void RegionWeather::setWeather(int weatherID) -{ - mWeather = weatherID; -} + RegionWeather::RegionWeather(const ESM::Region& region) + : mWeather(invalidWeatherID) + , mChances() + { + mChances.reserve(10); + mChances.push_back(region.mData.mClear); + mChances.push_back(region.mData.mCloudy); + mChances.push_back(region.mData.mFoggy); + mChances.push_back(region.mData.mOvercast); + mChances.push_back(region.mData.mRain); + mChances.push_back(region.mData.mThunder); + mChances.push_back(region.mData.mAsh); + mChances.push_back(region.mData.mBlight); + mChances.push_back(region.mData.mA); + mChances.push_back(region.mData.mB); + } -int RegionWeather::getWeather() -{ - // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. - // Note that the region weather will be expired periodically when the weather update timer expires. - if(mWeather == invalidWeatherID) + RegionWeather::RegionWeather(const ESM::RegionWeatherState& state) + : mWeather(state.mWeather) + , mChances(state.mChances) { - chooseNewWeather(); } - return mWeather; -} + RegionWeather::operator ESM::RegionWeatherState() const + { + ESM::RegionWeatherState state = + { + mWeather, + mChances + }; -void RegionWeather::chooseNewWeather() -{ - // All probabilities must add to 100 (responsibility of the user). - // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 - // and 70% will be greater than 30 (in theory). - int chance = Misc::Rng::rollDice(100) + 1; // 1..100 - int sum = 0; - int i = 0; - for(; static_cast(i) < mChances.size(); ++i) + return state; + } + + void RegionWeather::setChances(const std::vector& chances) { - sum += mChances[i]; - if(chance <= sum) + if(mChances.size() < chances.size()) { - mWeather = i; - return; + mChances.reserve(chances.size()); } - } - - // if we hit this path then the chances don't add to 100, choose a default weather instead - mWeather = 0; -} -MoonModel::MoonModel(const std::string& name) - : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) - , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) - , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) - , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) - , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) - , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) - , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) - , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) - , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) - , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) -{ - // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably - // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. - mSpeed = std::min(mSpeed, 180.0f / 23.0f); -} + int i = 0; + for(char chance : chances) + { + mChances[i] = chance; + i++; + } -MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const -{ - float rotationFromHorizon = angle(gameTime); - MWRender::MoonState state = + // Regional weather no longer supports the current type, select a new weather pattern. + if((static_cast(mWeather) >= mChances.size()) || (mChances[mWeather] == 0)) { - rotationFromHorizon, - mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. - phase(gameTime), - shadowBlend(rotationFromHorizon), - earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) - }; + chooseNewWeather(); + } + } - return state; -} + void RegionWeather::setWeather(int weatherID) + { + mWeather = weatherID; + } -inline float MoonModel::angle(const TimeStamp& gameTime) const -{ - // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the - // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. + int RegionWeather::getWeather() + { + // If the region weather was already set (by ChangeWeather, or by a previous call) then just return that value. + // Note that the region weather will be expired periodically when the weather update timer expires. + if(mWeather == invalidWeatherID) + { + chooseNewWeather(); + } - // When calculating the angle of the moon, several cases have to be taken into account: - // 1. Moon rises and then sets in one day. - // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). - // 3. Moon sets and then rises in one day. - float moonRiseHourToday = moonRiseHour(gameTime.getDay()); - float moonRiseAngleToday = 0; + return mWeather; + } - if(gameTime.getHour() < moonRiseHourToday) + void RegionWeather::chooseNewWeather() { - float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); - if(moonRiseHourYesterday < 24) + // All probabilities must add to 100 (responsibility of the user). + // If chances A and B has values 30 and 70 then by generating 100 numbers 1..100, 30% will be lesser or equal 30 + // and 70% will be greater than 30 (in theory). + int chance = Misc::Rng::rollDice(100) + 1; // 1..100 + int sum = 0; + int i = 0; + for(; static_cast(i) < mChances.size(); ++i) { - float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); - if(moonRiseAngleYesterday < 180) + sum += mChances[i]; + if(chance <= sum) { - // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. - moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + mWeather = i; + return; } } + + // if we hit this path then the chances don't add to 100, choose a default weather instead + mWeather = 0; } - else + + MoonModel::MoonModel(const std::string& name) + : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) + , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) + , mFadeOutStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Start")) + , mFadeOutFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_Out_Finish")) + , mAxisOffset(Fallback::Map::getFloat("Moons_" + name + "_Axis_Offset")) + , mSpeed(Fallback::Map::getFloat("Moons_" + name + "_Speed")) + , mDailyIncrement(Fallback::Map::getFloat("Moons_" + name + "_Daily_Increment")) + , mFadeStartAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_Start_Angle")) + , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) + , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { - moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); + // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably + // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. + mSpeed = std::min(mSpeed, 180.0f / 23.0f); } - if(moonRiseAngleToday >= 180) + MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { - // The moon set today, reset the angle to the horizon. - moonRiseAngleToday = 0; + float rotationFromHorizon = angle(gameTime); + MWRender::MoonState state = + { + rotationFromHorizon, + mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. + phase(gameTime), + shadowBlend(rotationFromHorizon), + earlyMoonShadowAlpha(rotationFromHorizon) * hourlyAlpha(gameTime.getHour()) + }; + + return state; } - return moonRiseAngleToday; -} + inline float MoonModel::angle(const TimeStamp& gameTime) const + { + // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to the + // opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon rise. -inline float MoonModel::moonRiseHour(unsigned int daysPassed) const -{ - // This arises from the start date of 16 Last Seed, 427 - // TODO: Find an alternate formula that doesn't rely on this day being fixed. - static const unsigned int startDay = 16; - - // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning - // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. - // Note that we don't modulo after adding the latest daily increment because other calculations need to - // know if doing so would cause the moon rise to be postponed until the next day (which happens when - // the moon rise hour is >= 24 in Morrowind). - return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); -} + // When calculating the angle of the moon, several cases have to be taken into account: + // 1. Moon rises and then sets in one day. + // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). + // 3. Moon sets and then rises in one day. + float moonRiseHourToday = moonRiseHour(gameTime.getDay()); + float moonRiseAngleToday = 0; -inline float MoonModel::rotation(float hours) const -{ - // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. - // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure - // of whole rotations that could be completed in a day. - return 15.0f * mSpeed * hours; -} - -MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const -{ - // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. - - // If the moon didn't rise yet today, use yesterday's moon phase. - if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) - return static_cast((gameTime.getDay() / 3) % 8); - else - return static_cast(((gameTime.getDay() + 1) / 3) % 8); -} + if(gameTime.getHour() < moonRiseHourToday) + { + float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); + if(moonRiseHourYesterday < 24) + { + float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); + if(moonRiseAngleYesterday < 180) + { + // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've travelled today. + moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + } + } + } + else + { + moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); + } -inline float MoonModel::shadowBlend(float angle) const -{ - // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk - // that is roughly the color of the sky, to a textured surface. - // Depending on the current angle, the following values describe the ratio between the textured moon - // and the solid disk: - // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 - // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) - // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 - // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) - float fadeAngle = mFadeStartAngle - mFadeEndAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float fadeStartAngle2 = 180.0f - mFadeStartAngle; - if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) - return (angle - mFadeEndAngle) / fadeAngle; - else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) - return 1.0f; - else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) - return (fadeEndAngle2 - angle) / fadeAngle; - else - return 0.0f; -} + if(moonRiseAngleToday >= 180) + { + // The moon set today, reset the angle to the horizon. + moonRiseAngleToday = 0; + } -inline float MoonModel::hourlyAlpha(float gameHour) const -{ - // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon - // appears and disappears. - // Depending on the current hour, the following values describe how transparent the moon is. - // 1. From Fade Out Start to Fade Out Finish: 1..0 - // 2. From Fade Out Finish to Fade In Start: 0 (transparent) - // 3. From Fade In Start to Fade In Finish: 0..1 - // 4. From Fade In Finish to Fade Out Start: 1 (solid) - if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) - return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); - else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) - return 0.0f; - else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) - return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); - else - return 1.0f; -} + return moonRiseAngleToday; + } -inline float MoonModel::earlyMoonShadowAlpha(float angle) const -{ - // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. - // Depending on the current angle, the following values describe how transparent the moon is. - // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 - // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) - // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 - // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) - float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; - float fadeEndAngle2 = 180.0f - mFadeEndAngle; - float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; - if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) - return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; - else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) - return 1.0f; - else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) - return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; - else - return 0.0f; -} + inline float MoonModel::moonRiseHour(unsigned int daysPassed) const + { + // This arises from the start date of 16 Last Seed, 427 + // TODO: Find an alternate formula that doesn't rely on this day being fixed. + static const unsigned int startDay = 16; + + // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning + // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. + // Note that we don't modulo after adding the latest daily increment because other calculations need to + // know if doing so would cause the moon rise to be postponed until the next day (which happens when + // the moon rise hour is >= 24 in Morrowind). + return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); + } -WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) - : mStore(store) - , mRendering(rendering) - , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) - , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) - , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) - , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) - , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) - , mNightFade(0, 0, 0, 1) - , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) - , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) - , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), - Fallback::Map::getFloat("Water_UnderwaterDayFog"), - Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), - Fallback::Map::getFloat("Water_UnderwaterNightFog")) - , mWeatherSettings() - , mMasser("Masser") - , mSecunda("Secunda") - , mWindSpeed(0.f) - , mCurrentWindSpeed(0.f) - , mNextWindSpeed(0.f) - , mIsStorm(false) - , mPrecipitation(false) - , mStormDirection(0,1,0) - , mCurrentRegion() - , mTimePassed(0) - , mFastForward(false) - , mWeatherUpdateTime(mHoursBetweenWeatherChanges) - , mTransitionFactor(0) - , mNightDayMode(Default) - , mCurrentWeather(0) - , mNextWeather(0) - , mQueuedWeather(0) - , mRegions() - , mResult() - , mAmbientSound(nullptr) - , mPlayingSoundID() -{ - mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime; - mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; - mTimeSettings.mDayEnd = mSunsetTime; - - mTimeSettings.addSetting("Sky"); - mTimeSettings.addSetting("Ambient"); - mTimeSettings.addSetting("Fog"); - mTimeSettings.addSetting("Sun"); - - // Morrowind handles stars settings differently for other ones - mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); - mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); - mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); - - WeatherSetting starSetting = { - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, - mTimeSettings.mStarsPostSunsetStart, - mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart - }; - - mTimeSettings.mSunriseTransitions["Stars"] = starSetting; - - mWeatherSettings.reserve(10); - // These distant land fog factor and offset values are the defaults MGE XE provides. Should be - // provided by settings somewhere? - addWeather("Clear", 1.0f, 0.0f); // 0 - addWeather("Cloudy", 0.9f, 0.0f); // 1 - addWeather("Foggy", 0.2f, 30.0f); // 2 - addWeather("Overcast", 0.7f, 0.0f); // 3 - addWeather("Rain", 0.5f, 10.0f); // 4 - addWeather("Thunderstorm", 0.5f, 20.0f); // 5 - addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 - addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 - addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 - addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 - - Store::iterator it = store.get().begin(); - for(; it != store.get().end(); ++it) + inline float MoonModel::rotation(float hours) const { - std::string regionID = Misc::StringUtils::lowerCase(it->mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); + // 15 degrees per hour was reverse engineered from the rotation matrices of the Morrowind scene graph. + // Note that this correlates to 360 / 24, which is a full rotation every 24 hours, so speed is a measure + // of whole rotations that could be completed in a day. + return 15.0f * mSpeed * hours; } - forceWeather(0); -} + MWRender::MoonState::Phase MoonModel::phase(const TimeStamp& gameTime) const + { + // Morrowind starts with a full moon on 16 Last Seed and then begins to wane 17 Last Seed, working on 3 day phase cycle. -WeatherManager::~WeatherManager() -{ - stopSounds(); -} + // If the moon didn't rise yet today, use yesterday's moon phase. + if(gameTime.getHour() < moonRiseHour(gameTime.getDay())) + return static_cast((gameTime.getDay() / 3) % 8); + else + return static_cast(((gameTime.getDay() + 1) / 3) % 8); + } -void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) -{ - // In Morrowind, this seems to have the following behavior, when applied to the current region: - // - When there is no transition in progress, start transitioning to the new weather. - // - If there is a transition in progress, queue up the transition and process it when the current one completes. - // - If there is a transition in progress, and a queued transition, overwrite the queued transition. - // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, - // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. - // If the region isn't current, Morrowind will store the new weather for the region in question. - - if(weatherID < mWeatherSettings.size()) + inline float MoonModel::shadowBlend(float angle) const { - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) - { - it->second.setWeather(weatherID); - regionalWeatherChanged(it->first, it->second); - } + // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk + // that is roughly the color of the sky, to a textured surface. + // Depending on the current angle, the following values describe the ratio between the textured moon + // and the solid disk: + // 1. From Fade End Angle 1 to Fade Start Angle 1 (during moon rise): 0..1 + // 2. From Fade Start Angle 1 to Fade Start Angle 2 (between moon rise and moon set): 1 (textured) + // 3. From Fade Start Angle 2 to Fade End Angle 2 (during moon set): 1..0 + // 4. From Fade End Angle 2 to Fade End Angle 1 (between moon set and moon rise): 0 (solid disk) + float fadeAngle = mFadeStartAngle - mFadeEndAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float fadeStartAngle2 = 180.0f - mFadeStartAngle; + if((angle >= mFadeEndAngle) && (angle < mFadeStartAngle)) + return (angle - mFadeEndAngle) / fadeAngle; + else if((angle >= mFadeStartAngle) && (angle < fadeStartAngle2)) + return 1.0f; + else if((angle >= fadeStartAngle2) && (angle < fadeEndAngle2)) + return (fadeEndAngle2 - angle) / fadeAngle; + else + return 0.0f; } -} -void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) -{ - // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. - // In Morrowind, this seems to have the following behavior when applied to the current region: - // - If the region supports the current weather, no change in current weather occurs. - // - If the region no longer supports the current weather, and there is no transition in progress, begin to - // transition to a new supported weather type. - // - If the region no longer supports the current weather, and there is a transition in progress, queue a - // transition to a new supported weather type. - - std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); - std::map::iterator it = mRegions.find(lowerCaseRegionID); - if(it != mRegions.end()) + inline float MoonModel::hourlyAlpha(float gameHour) const { - it->second.setChances(chances); - regionalWeatherChanged(it->first, it->second); + // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon + // appears and disappears. + // Depending on the current hour, the following values describe how transparent the moon is. + // 1. From Fade Out Start to Fade Out Finish: 1..0 + // 2. From Fade Out Finish to Fade In Start: 0 (transparent) + // 3. From Fade In Start to Fade In Finish: 0..1 + // 4. From Fade In Finish to Fade Out Start: 1 (solid) + if((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) + return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); + else if((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) + return 0.0f; + else if((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) + return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); + else + return 1.0f; } -} -void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) -{ - // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to - // be changed immediately, and any transitions for the previous region discarded. + inline float MoonModel::earlyMoonShadowAlpha(float angle) const { - std::map::iterator it = mRegions.find(playerRegion); - if(it != mRegions.end() && playerRegion != mCurrentRegion) - { - mCurrentRegion = playerRegion; - forceWeather(it->second.getWeather()); - } + // The Moon Shadow Early Fade Angle describes an arc relative to Fade End Angle. + // Depending on the current angle, the following values describe how transparent the moon is. + // 1. From Moon Shadow Early Fade Angle 1 to Fade End Angle 1 (during moon rise): 0..1 + // 2. From Fade End Angle 1 to Fade End Angle 2 (between moon rise and moon set): 1 (solid) + // 3. From Fade End Angle 2 to Moon Shadow Early Fade Angle 2 (during moon set): 1..0 + // 4. From Moon Shadow Early Fade Angle 2 to Moon Shadow Early Fade Angle 1: 0 (transparent) + float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; + float fadeEndAngle2 = 180.0f - mFadeEndAngle; + float moonShadowEarlyFadeAngle2 = fadeEndAngle2 + mMoonShadowEarlyFadeAngle; + if((angle >= moonShadowEarlyFadeAngle1) && (angle < mFadeEndAngle)) + return (angle - moonShadowEarlyFadeAngle1) / mMoonShadowEarlyFadeAngle; + else if((angle >= mFadeEndAngle) && (angle < fadeEndAngle2)) + return 1.0f; + else if((angle >= fadeEndAngle2) && (angle < moonShadowEarlyFadeAngle2)) + return (moonShadowEarlyFadeAngle2 - angle) / mMoonShadowEarlyFadeAngle; + else + return 0.0f; } -} - -float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) -{ - float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); - if (currentSpeed == 0.f) - currentSpeed = targetSpeed; - float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; - float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; + WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, MWWorld::ESMStore& store) + : mStore(store) + , mRendering(rendering) + , mSunriseTime(Fallback::Map::getFloat("Weather_Sunrise_Time")) + , mSunsetTime(Fallback::Map::getFloat("Weather_Sunset_Time")) + , mSunriseDuration(Fallback::Map::getFloat("Weather_Sunrise_Duration")) + , mSunsetDuration(Fallback::Map::getFloat("Weather_Sunset_Duration")) + , mSunPreSunsetTime(Fallback::Map::getFloat("Weather_Sun_Pre-Sunset_Time")) + , mNightFade(0, 0, 0, 1) + , mHoursBetweenWeatherChanges(Fallback::Map::getFloat("Weather_Hours_Between_Weather_Changes")) + , mRainSpeed(Fallback::Map::getFloat("Weather_Precip_Gravity")) + , mUnderwaterFog(Fallback::Map::getFloat("Water_UnderwaterSunriseFog"), + Fallback::Map::getFloat("Water_UnderwaterDayFog"), + Fallback::Map::getFloat("Water_UnderwaterSunsetFog"), + Fallback::Map::getFloat("Water_UnderwaterNightFog")) + , mWeatherSettings() + , mMasser("Masser") + , mSecunda("Secunda") + , mWindSpeed(0.f) + , mCurrentWindSpeed(0.f) + , mNextWindSpeed(0.f) + , mIsStorm(false) + , mPrecipitation(false) + , mStormDirection(Weather::defaultDirection()) + , mCurrentRegion() + , mTimePassed(0) + , mFastForward(false) + , mWeatherUpdateTime(mHoursBetweenWeatherChanges) + , mTransitionFactor(0) + , mNightDayMode(Default) + , mCurrentWeather(0) + , mNextWeather(0) + , mQueuedWeather(0) + , mRegions() + , mResult() + , mAmbientSound(nullptr) + , mPlayingSoundID() + { + mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; + mTimeSettings.mNightEnd = mSunriseTime; + mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; + mTimeSettings.mDayEnd = mSunsetTime; + + mTimeSettings.addSetting("Sky"); + mTimeSettings.addSetting("Ambient"); + mTimeSettings.addSetting("Fog"); + mTimeSettings.addSetting("Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = Fallback::Map::getFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = Fallback::Map::getFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = Fallback::Map::getFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart + }; - if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) - currentSpeed = updatedSpeed; + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; + + mWeatherSettings.reserve(10); + // These distant land fog factor and offset values are the defaults MGE XE provides. Should be + // provided by settings somewhere? + addWeather("Clear", 1.0f, 0.0f); // 0 + addWeather("Cloudy", 0.9f, 0.0f); // 1 + addWeather("Foggy", 0.2f, 30.0f); // 2 + addWeather("Overcast", 0.7f, 0.0f); // 3 + addWeather("Rain", 0.5f, 10.0f); // 4 + addWeather("Thunderstorm", 0.5f, 20.0f); // 5 + addWeather("Ashstorm", 0.2f, 50.0f, "meshes\\ashcloud.nif"); // 6 + addWeather("Blight", 0.2f, 60.0f, "meshes\\blightcloud.nif"); // 7 + addWeather("Snow", 0.5f, 40.0f, "meshes\\snow.nif"); // 8 + addWeather("Blizzard", 0.16f, 70.0f, "meshes\\blizzard.nif"); // 9 + + Store::iterator it = store.get().begin(); + for(; it != store.get().end(); ++it) + { + std::string regionID = Misc::StringUtils::lowerCase(it->mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(*it))); + } - return currentSpeed; -} + forceWeather(0); + } -void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) -{ - MWWorld::ConstPtr player = MWMechanics::getPlayer(); + WeatherManager::~WeatherManager() + { + stopSounds(); + } - if(!paused || mFastForward) + void WeatherManager::changeWeather(const std::string& regionID, const unsigned int weatherID) { - // Add new transitions when either the player's current external region changes. - std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); - if(updateWeatherTime() || updateWeatherRegion(playerRegion)) + // In Morrowind, this seems to have the following behavior, when applied to the current region: + // - When there is no transition in progress, start transitioning to the new weather. + // - If there is a transition in progress, queue up the transition and process it when the current one completes. + // - If there is a transition in progress, and a queued transition, overwrite the queued transition. + // - If multiple calls to ChangeWeather are made while paused (console up), only the last call will be used, + // meaning that if there was no transition in progress, only the last ChangeWeather will be processed. + // If the region isn't current, Morrowind will store the new weather for the region in question. + + if(weatherID < mWeatherSettings.size()) { - std::map::iterator it = mRegions.find(mCurrentRegion); + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); if(it != mRegions.end()) { - addWeatherTransition(it->second.getWeather()); + it->second.setWeather(weatherID); + regionalWeatherChanged(it->first, it->second); } } - - updateWeatherTransitions(duration); } - bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; - if (isExterior && !isDay) - mNightDayMode = ExteriorNight; - else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) - mNightDayMode = InteriorDay; - else - mNightDayMode = Default; - - if(!isExterior) + void WeatherManager::modRegion(const std::string& regionID, const std::vector& chances) { - mRendering.setSkyEnabled(false); - stopSounds(); - mWindSpeed = 0.f; - mCurrentWindSpeed = 0.f; - mNextWindSpeed = 0.f; - return; - } + // Sets the region's probability for various weather patterns. Note that this appears to be saved permanently. + // In Morrowind, this seems to have the following behavior when applied to the current region: + // - If the region supports the current weather, no change in current weather occurs. + // - If the region no longer supports the current weather, and there is no transition in progress, begin to + // transition to a new supported weather type. + // - If the region no longer supports the current weather, and there is a transition in progress, queue a + // transition to a new supported weather type. - calculateWeatherResult(time.getHour(), duration, paused); + std::string lowerCaseRegionID = Misc::StringUtils::lowerCase(regionID); + std::map::iterator it = mRegions.find(lowerCaseRegionID); + if(it != mRegions.end()) + { + it->second.setChances(chances); + regionalWeatherChanged(it->first, it->second); + } + } - if (!paused) + void WeatherManager::playerTeleported(const std::string& playerRegion, bool isExterior) { - mWindSpeed = mResult.mWindSpeed; - mCurrentWindSpeed = mResult.mCurrentWindSpeed; - mNextWindSpeed = mResult.mNextWindSpeed; + // If the player teleports to an outdoors cell in a new region (for instance, by travelling), the weather needs to + // be changed immediately, and any transitions for the previous region discarded. + { + std::map::iterator it = mRegions.find(playerRegion); + if(it != mRegions.end() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; + forceWeather(it->second.getWeather()); + } + } } - mIsStorm = mResult.mIsStorm; + float WeatherManager::calculateWindSpeed(int weatherId, float currentSpeed) + { + float targetSpeed = std::min(8.0f * mWeatherSettings[weatherId].mWindSpeed, 70.f); + if (currentSpeed == 0.f) + currentSpeed = targetSpeed; + + float multiplier = mWeatherSettings[weatherId].mRainEffect.empty() ? 1.f : 0.5f; + float updatedSpeed = (Misc::Rng::rollClosedProbability() - 0.5f) * multiplier * targetSpeed + currentSpeed; - // For some reason Ash Storm is not considered as a precipitation weather in game - mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) - && mResult.mParticleEffect != "meshes\\ashcloud.nif"; + if (updatedSpeed > 0.5f * targetSpeed && updatedSpeed < 2.f * targetSpeed) + currentSpeed = updatedSpeed; - if (mIsStorm) + return currentSpeed; + } + + void WeatherManager::update(float duration, bool paused, const TimeStamp& time, bool isExterior) { - osg::Vec3f stormDirection(0, 1, 0); - if (mResult.mParticleEffect == "meshes\\ashcloud.nif" || mResult.mParticleEffect == "meshes\\blightcloud.nif") + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + + if(!paused || mFastForward) { - osg::Vec3f playerPos (MWMechanics::getPlayer().getRefData().getPosition().asVec3()); - playerPos.z() = 0; - osg::Vec3f redMountainPos (25000, 70000, 0); - stormDirection = (playerPos - redMountainPos); - stormDirection.normalize(); + // Add new transitions when either the player's current external region changes. + std::string playerRegion = Misc::StringUtils::lowerCase(player.getCell()->getCell()->mRegion); + if(updateWeatherTime() || updateWeatherRegion(playerRegion)) + { + std::map::iterator it = mRegions.find(mCurrentRegion); + if(it != mRegions.end()) + { + addWeatherTransition(it->second.getWeather()); + } + } + + updateWeatherTransitions(duration); } - mStormDirection = stormDirection; - mRendering.getSkyManager()->setStormDirection(mStormDirection); - } - // disable sun during night - if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) - mRendering.getSkyManager()->sunDisable(); - else - mRendering.getSkyManager()->sunEnable(); + bool isDay = time.getHour() >= mSunriseTime && time.getHour() <= mTimeSettings.mNightStart; + if (isExterior && !isDay) + mNightDayMode = ExteriorNight; + else if (!isExterior && isDay && mWeatherSettings[mCurrentWeather].mGlareView >= 0.5f) + mNightDayMode = InteriorDay; + else + mNightDayMode = Default; - // Update the sun direction. Run it east to west at a fixed angle from overhead. - // The sun's speed at day and night may differ, since mSunriseTime and mNightStart - // mark when the sun is level with the horizon. - { - // Shift times into a 24-hour window beginning at mSunriseTime... - float adjustedHour = time.getHour(); - float adjustedNightStart = mTimeSettings.mNightStart; - if ( time.getHour() < mSunriseTime ) - adjustedHour += 24.f; - if ( mTimeSettings.mNightStart < mSunriseTime ) - adjustedNightStart += 24.f; - - const bool is_night = adjustedHour >= adjustedNightStart; - const float dayDuration = adjustedNightStart - mSunriseTime; - const float nightDuration = 24.f - dayDuration; - - double theta; - if ( !is_night ) + if(!isExterior) { - theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; + mRendering.setSkyEnabled(false); + stopSounds(); + mWindSpeed = 0.f; + mCurrentWindSpeed = 0.f; + mNextWindSpeed = 0.f; + return; } - else + + calculateWeatherResult(time.getHour(), duration, paused); + + if (!paused) { - theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + mWindSpeed = mResult.mWindSpeed; + mCurrentWindSpeed = mResult.mCurrentWindSpeed; + mNextWindSpeed = mResult.mNextWindSpeed; } - osg::Vec3f final( - static_cast(cos(theta)), - -0.268f, // approx tan( -15 degrees ) - static_cast(sin(theta))); - mRendering.setSunDirection( final * -1 ); - } + mIsStorm = mResult.mIsStorm; - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); + // For some reason Ash Storm is not considered as a precipitation weather in game + mPrecipitation = !(mResult.mParticleEffect.empty() && mResult.mRainEffect.empty()) + && mResult.mParticleEffect != "meshes\\ashcloud.nif"; - float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; - float glareFade = 1.f; - if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) - glareFade = 0.f; - else if (time.getHour() < peakHour) - glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); - else - glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); + mStormDirection = calculateStormDirection(mResult.mParticleEffect); + mRendering.getSkyManager()->setStormParticleDirection(mStormDirection); - mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); + // disable sun during night + if (time.getHour() >= mTimeSettings.mNightStart || time.getHour() <= mSunriseTime) + mRendering.getSkyManager()->sunDisable(); + else + mRendering.getSkyManager()->sunEnable(); - mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); - mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); + // Update the sun direction. Run it east to west at a fixed angle from overhead. + // The sun's speed at day and night may differ, since mSunriseTime and mNightStart + // mark when the sun is level with the horizon. + { + // Shift times into a 24-hour window beginning at mSunriseTime... + float adjustedHour = time.getHour(); + float adjustedNightStart = mTimeSettings.mNightStart; + if ( time.getHour() < mSunriseTime ) + adjustedHour += 24.f; + if ( mTimeSettings.mNightStart < mSunriseTime ) + adjustedNightStart += 24.f; + + const bool is_night = adjustedHour >= adjustedNightStart; + const float dayDuration = adjustedNightStart - mSunriseTime; + const float nightDuration = 24.f - dayDuration; + + double theta; + if ( !is_night ) + { + theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; + } + else + { + theta = static_cast(osg::PI) - static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; + } - mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, - mResult.mDLFogOffset/100.0f, mResult.mFogColor); - mRendering.setAmbientColour(mResult.mAmbientColor); - mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); + osg::Vec3f final( + static_cast(cos(theta)), + -0.268f, // approx tan( -15 degrees ) + static_cast(sin(theta))); + mRendering.setSunDirection( final * -1 ); + } - mRendering.getSkyManager()->setWeather(mResult); + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); - // Play sounds - if (mPlayingSoundID != mResult.mAmbientLoopSoundID) - { - stopSounds(); - if (!mResult.mAmbientLoopSoundID.empty()) - mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( - mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, - MWSound::Type::Sfx, MWSound::PlayMode::Loop - ); - mPlayingSoundID = mResult.mAmbientLoopSoundID; - } - else if (mAmbientSound) - mAmbientSound->setVolume(mResult.mAmbientSoundVolume); -} + float peakHour = mSunriseTime + (mTimeSettings.mNightStart - mSunriseTime) / 2; + float glareFade = 1.f; + if (time.getHour() < mSunriseTime || time.getHour() > mTimeSettings.mNightStart) + glareFade = 0.f; + else if (time.getHour() < peakHour) + glareFade = 1.f - (peakHour - time.getHour()) / (peakHour - mSunriseTime); + else + glareFade = 1.f - (time.getHour() - peakHour) / (mTimeSettings.mNightStart - peakHour); -void WeatherManager::stopSounds() -{ - if (mAmbientSound) - MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); - mAmbientSound = nullptr; - mPlayingSoundID.clear(); -} + mRendering.getSkyManager()->setGlareTimeOfDayFade(glareFade); -float WeatherManager::getWindSpeed() const -{ - return mWindSpeed; -} + mRendering.getSkyManager()->setMasserState(mMasser.calculateState(time)); + mRendering.getSkyManager()->setSecundaState(mSecunda.calculateState(time)); -bool WeatherManager::isInStorm() const -{ - return mIsStorm; -} + mRendering.configureFog(mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, + mResult.mDLFogOffset/100.0f, mResult.mFogColor); + mRendering.setAmbientColour(mResult.mAmbientColor); + mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade); -osg::Vec3f WeatherManager::getStormDirection() const -{ - return mStormDirection; -} + mRendering.getSkyManager()->setWeather(mResult); -void WeatherManager::advanceTime(double hours, bool incremental) -{ - // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are - // immediately applied, regardless of whatever transition time might have been remaining. - mTimePassed += hours; - mFastForward = !incremental ? true : mFastForward; -} + // Play sounds + if (mPlayingSoundID != mResult.mAmbientLoopSoundID) + { + stopSounds(); + if (!mResult.mAmbientLoopSoundID.empty()) + mAmbientSound = MWBase::Environment::get().getSoundManager()->playSound( + mResult.mAmbientLoopSoundID, mResult.mAmbientSoundVolume, 1.0, + MWSound::Type::Sfx, MWSound::PlayMode::Loop + ); + mPlayingSoundID = mResult.mAmbientLoopSoundID; + } + else if (mAmbientSound) + mAmbientSound->setVolume(mResult.mAmbientSoundVolume); + } -unsigned int WeatherManager::getWeatherID() const -{ - return mCurrentWeather; -} + void WeatherManager::stopSounds() + { + if (mAmbientSound) + MWBase::Environment::get().getSoundManager()->stopSound(mAmbientSound); + mAmbientSound = nullptr; + mPlayingSoundID.clear(); + } -NightDayMode WeatherManager::getNightDayMode() const -{ - return mNightDayMode; -} + float WeatherManager::getWindSpeed() const + { + return mWindSpeed; + } -bool WeatherManager::useTorches(float hour) const -{ - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + bool WeatherManager::isInStorm() const + { + return mIsStorm; + } - return isDark && !mPrecipitation; -} + osg::Vec3f WeatherManager::getStormDirection() const + { + return mStormDirection; + } -void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) -{ - ESM::WeatherState state; - state.mCurrentRegion = mCurrentRegion; - state.mTimePassed = mTimePassed; - state.mFastForward = mFastForward; - state.mWeatherUpdateTime = mWeatherUpdateTime; - state.mTransitionFactor = mTransitionFactor; - state.mCurrentWeather = mCurrentWeather; - state.mNextWeather = mNextWeather; - state.mQueuedWeather = mQueuedWeather; - - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) + void WeatherManager::advanceTime(double hours, bool incremental) { - state.mRegions.insert(std::make_pair(it->first, it->second)); + // In Morrowind, when the player sleeps/waits, serves jail time, travels, or trains, all weather transitions are + // immediately applied, regardless of whatever transition time might have been remaining. + mTimePassed += hours; + mFastForward = !incremental ? true : mFastForward; } - writer.startRecord(ESM::REC_WTHR); - state.save(writer); - writer.endRecord(ESM::REC_WTHR); -} + unsigned int WeatherManager::getWeatherID() const + { + return mCurrentWeather; + } -bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) -{ - if(ESM::REC_WTHR == type) + NightDayMode WeatherManager::getNightDayMode() const + { + return mNightDayMode; + } + + bool WeatherManager::useTorches(float hour) const + { + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; + + return isDark && !mPrecipitation; + } + + void WeatherManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { - static const int oldestCompatibleSaveFormat = 2; - if(reader.getFormat() < oldestCompatibleSaveFormat) + ESM::WeatherState state; + state.mCurrentRegion = mCurrentRegion; + state.mTimePassed = mTimePassed; + state.mFastForward = mFastForward; + state.mWeatherUpdateTime = mWeatherUpdateTime; + state.mTransitionFactor = mTransitionFactor; + state.mCurrentWeather = mCurrentWeather; + state.mNextWeather = mNextWeather; + state.mQueuedWeather = mQueuedWeather; + + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) { - // Weather state isn't really all that important, so to preserve older save games, we'll just discard the - // older weather records, rather than fail to handle the record. - reader.skipRecord(); + state.mRegions.insert(std::make_pair(it->first, it->second)); } - else + + writer.startRecord(ESM::REC_WTHR); + state.save(writer); + writer.endRecord(ESM::REC_WTHR); + } + + bool WeatherManager::readRecord(ESM::ESMReader& reader, uint32_t type) + { + if(ESM::REC_WTHR == type) { - ESM::WeatherState state; - state.load(reader); - - mCurrentRegion.swap(state.mCurrentRegion); - mTimePassed = state.mTimePassed; - mFastForward = state.mFastForward; - mWeatherUpdateTime = state.mWeatherUpdateTime; - mTransitionFactor = state.mTransitionFactor; - mCurrentWeather = state.mCurrentWeather; - mNextWeather = state.mNextWeather; - mQueuedWeather = state.mQueuedWeather; - - mRegions.clear(); - importRegions(); - - for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) + static const int oldestCompatibleSaveFormat = 2; + if(reader.getFormat() < oldestCompatibleSaveFormat) { - std::map::iterator found = mRegions.find(it->first); - if (found != mRegions.end()) + // Weather state isn't really all that important, so to preserve older save games, we'll just discard the + // older weather records, rather than fail to handle the record. + reader.skipRecord(); + } + else + { + ESM::WeatherState state; + state.load(reader); + + mCurrentRegion.swap(state.mCurrentRegion); + mTimePassed = state.mTimePassed; + mFastForward = state.mFastForward; + mWeatherUpdateTime = state.mWeatherUpdateTime; + mTransitionFactor = state.mTransitionFactor; + mCurrentWeather = state.mCurrentWeather; + mNextWeather = state.mNextWeather; + mQueuedWeather = state.mQueuedWeather; + + mRegions.clear(); + importRegions(); + + for(std::map::iterator it = state.mRegions.begin(); it != state.mRegions.end(); ++it) { - found->second = RegionWeather(it->second); + std::map::iterator found = mRegions.find(it->first); + if (found != mRegions.end()) + { + found->second = RegionWeather(it->second); + } } } + + return true; } - return true; + return false; } - return false; -} + void WeatherManager::clear() + { + stopSounds(); -void WeatherManager::clear() -{ - stopSounds(); - - mCurrentRegion = ""; - mTimePassed = 0.0f; - mWeatherUpdateTime = 0.0f; - forceWeather(0); - mRegions.clear(); - importRegions(); -} + mCurrentRegion = ""; + mTimePassed = 0.0f; + mWeatherUpdateTime = 0.0f; + forceWeather(0); + mRegions.clear(); + importRegions(); + } -inline void WeatherManager::addWeather(const std::string& name, - float dlFactor, float dlOffset, - const std::string& particleEffect) -{ - static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); + inline void WeatherManager::addWeather(const std::string& name, + float dlFactor, float dlOffset, + const std::string& particleEffect) + { + static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); - Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); + Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); - mWeatherSettings.push_back(weather); -} + mWeatherSettings.push_back(weather); + } -inline void WeatherManager::importRegions() -{ - for(const ESM::Region& region : mStore.get()) + inline void WeatherManager::importRegions() { - std::string regionID = Misc::StringUtils::lowerCase(region.mId); - mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + for(const ESM::Region& region : mStore.get()) + { + std::string regionID = Misc::StringUtils::lowerCase(region.mId); + mRegions.insert(std::make_pair(regionID, RegionWeather(region))); + } } -} -inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) -{ - // If the region is current, then add a weather transition for it. - MWWorld::ConstPtr player = MWMechanics::getPlayer(); - if(player.isInCell()) + inline void WeatherManager::regionalWeatherChanged(const std::string& regionID, RegionWeather& region) { - if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + // If the region is current, then add a weather transition for it. + MWWorld::ConstPtr player = MWMechanics::getPlayer(); + if(player.isInCell()) { - addWeatherTransition(region.getWeather()); + if(Misc::StringUtils::ciEqual(regionID, mCurrentRegion)) + { + addWeatherTransition(region.getWeather()); + } } } -} -inline bool WeatherManager::updateWeatherTime() -{ - mWeatherUpdateTime -= mTimePassed; - mTimePassed = 0.0f; - if(mWeatherUpdateTime <= 0.0f) + inline bool WeatherManager::updateWeatherTime() { - // Expire all regional weather, so that any call to getWeather() will return a new weather ID. - std::map::iterator it = mRegions.begin(); - for(; it != mRegions.end(); ++it) + mWeatherUpdateTime -= mTimePassed; + mTimePassed = 0.0f; + if(mWeatherUpdateTime <= 0.0f) { - it->second.setWeather(invalidWeatherID); - } + // Expire all regional weather, so that any call to getWeather() will return a new weather ID. + std::map::iterator it = mRegions.begin(); + for(; it != mRegions.end(); ++it) + { + it->second.setWeather(invalidWeatherID); + } - mWeatherUpdateTime += mHoursBetweenWeatherChanges; + mWeatherUpdateTime += mHoursBetweenWeatherChanges; - return true; - } + return true; + } - return false; -} + return false; + } -inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) -{ - if(!playerRegion.empty() && playerRegion != mCurrentRegion) + inline bool WeatherManager::updateWeatherRegion(const std::string& playerRegion) { - mCurrentRegion = playerRegion; + if(!playerRegion.empty() && playerRegion != mCurrentRegion) + { + mCurrentRegion = playerRegion; - return true; - } + return true; + } - return false; -} + return false; + } -inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) -{ - // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last - // weather type set, regardless of the remaining transition time. - if(!mFastForward && inTransition()) + inline void WeatherManager::updateWeatherTransitions(const float elapsedRealSeconds) { - const float delta = mWeatherSettings[mNextWeather].transitionDelta(); - mTransitionFactor -= elapsedRealSeconds * delta; - if(mTransitionFactor <= 0.0f) + // When a player chooses to train, wait, or serves jail time, any transitions will be fast forwarded to the last + // weather type set, regardless of the remaining transition time. + if(!mFastForward && inTransition()) { - mCurrentWeather = mNextWeather; - mNextWeather = mQueuedWeather; - mQueuedWeather = invalidWeatherID; + const float delta = mWeatherSettings[mNextWeather].transitionDelta(); + mTransitionFactor -= elapsedRealSeconds * delta; + if(mTransitionFactor <= 0.0f) + { + mCurrentWeather = mNextWeather; + mNextWeather = mQueuedWeather; + mQueuedWeather = invalidWeatherID; - // We may have begun processing the queued transition, so we need to apply the remaining time towards it. - if(inTransition()) + // We may have begun processing the queued transition, so we need to apply the remaining time towards it. + if(inTransition()) + { + const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); + const float remainingSeconds = -(mTransitionFactor / delta); + mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + } + else + { + mTransitionFactor = 0.0f; + } + } + } + else + { + if(mQueuedWeather != invalidWeatherID) { - const float newDelta = mWeatherSettings[mNextWeather].transitionDelta(); - const float remainingSeconds = -(mTransitionFactor / delta); - mTransitionFactor = 1.0f - (remainingSeconds * newDelta); + mCurrentWeather = mQueuedWeather; } - else + else if(mNextWeather != invalidWeatherID) { - mTransitionFactor = 0.0f; + mCurrentWeather = mNextWeather; } + + mNextWeather = invalidWeatherID; + mQueuedWeather = invalidWeatherID; + mFastForward = false; } } - else - { - if(mQueuedWeather != invalidWeatherID) - { - mCurrentWeather = mQueuedWeather; - } - else if(mNextWeather != invalidWeatherID) - { - mCurrentWeather = mNextWeather; - } + inline void WeatherManager::forceWeather(const int weatherID) + { + mTransitionFactor = 0.0f; + mCurrentWeather = weatherID; mNextWeather = invalidWeatherID; mQueuedWeather = invalidWeatherID; - mFastForward = false; } -} - -inline void WeatherManager::forceWeather(const int weatherID) -{ - mTransitionFactor = 0.0f; - mCurrentWeather = weatherID; - mNextWeather = invalidWeatherID; - mQueuedWeather = invalidWeatherID; -} -inline bool WeatherManager::inTransition() -{ - return mNextWeather != invalidWeatherID; -} - -inline void WeatherManager::addWeatherTransition(const int weatherID) -{ - // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if - // no transition is in progress, otherwise it queues it to be transitioned. - - assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); - - if(!inTransition() && (weatherID != mCurrentWeather)) - { - mNextWeather = weatherID; - mTransitionFactor = 1.0f; - } - else if(inTransition() && (weatherID != mNextWeather)) + inline bool WeatherManager::inTransition() { - mQueuedWeather = weatherID; + return mNextWeather != invalidWeatherID; } -} -inline void WeatherManager::calculateWeatherResult(const float gameHour, - const float elapsedSeconds, - const bool isPaused) -{ - float flash = 0.0f; - if(!inTransition()) - { - calculateResult(mCurrentWeather, gameHour); - flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); - } - else + inline void WeatherManager::addWeatherTransition(const int weatherID) { - calculateTransitionResult(1 - mTransitionFactor, gameHour); - float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, - elapsedSeconds, - isPaused); - float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, - elapsedSeconds, - isPaused); - flash = currentFlash + nextFlash; - } - osg::Vec4f flashColor(flash, flash, flash, 0.0f); + // In order to work like ChangeWeather expects, this method begins transitioning to the new weather immediately if + // no transition is in progress, otherwise it queues it to be transitioned. - mResult.mFogColor += flashColor; - mResult.mAmbientColor += flashColor; - mResult.mSunColor += flashColor; -} + assert(weatherID >= 0 && static_cast(weatherID) < mWeatherSettings.size()); -inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) -{ - const Weather& current = mWeatherSettings[weatherID]; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mCloudBlendFactor = 0; - mResult.mNextWindSpeed = 0; - mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); - mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; - - mResult.mCloudSpeed = current.mCloudSpeed; - mResult.mGlareView = current.mGlareView; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; - mResult.mAmbientSoundVolume = 1.f; - mResult.mPrecipitationAlpha = 1.f; - - mResult.mIsStorm = current.mIsStorm; - - mResult.mRainSpeed = current.mRainSpeed; - mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mRainDiameter = current.mRainDiameter; - mResult.mRainMinHeight = current.mRainMinHeight; - mResult.mRainMaxHeight = current.mRainMaxHeight; - mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; - - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; - - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); - mResult.mDLFogFactor = current.mDL.FogFactor; - mResult.mDLFogOffset = current.mDL.FogOffset; - - WeatherSetting setting = mTimeSettings.getSetting("Sun"); - float preSunsetTime = setting.mPreSunsetTime; - - if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) - { - float factor = 1.f; - if (preSunsetTime > 0) - factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; - factor = std::min(1.f, factor); - mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); - // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because - // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline - // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. - // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. - mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); - for (int i=0; i<3; ++i) - mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + if(!inTransition() && (weatherID != mCurrentWeather)) + { + mNextWeather = weatherID; + mTransitionFactor = 1.0f; + } + else if(inTransition() && (weatherID != mNextWeather)) + { + mQueuedWeather = weatherID; + } } - else - mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - if (gameHour >= mTimeSettings.mDayEnd) + inline void WeatherManager::calculateWeatherResult(const float gameHour, + const float elapsedSeconds, + const bool isPaused) { - // sunset - float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); - fade = fade*fade; - mResult.mSunDiscColor.a() = 1.f - fade; - } - else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) - { - // sunrise - mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; - } - else - mResult.mSunDiscColor.a() = 1; - -} - -inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) -{ - calculateResult(mCurrentWeather, gameHour); - const MWRender::WeatherResult current = mResult; - calculateResult(mNextWeather, gameHour); - const MWRender::WeatherResult other = mResult; - - mResult.mCloudTexture = current.mCloudTexture; - mResult.mNextCloudTexture = other.mCloudTexture; - mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); - - mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); - mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); - mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); - - mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); - mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); - mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); - mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); - mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + float flash = 0.0f; + if(!inTransition()) + { + calculateResult(mCurrentWeather, gameHour); + flash = mWeatherSettings[mCurrentWeather].calculateThunder(1.0f, elapsedSeconds, isPaused); + } + else + { + calculateTransitionResult(1 - mTransitionFactor, gameHour); + float currentFlash = mWeatherSettings[mCurrentWeather].calculateThunder(mTransitionFactor, + elapsedSeconds, + isPaused); + float nextFlash = mWeatherSettings[mNextWeather].calculateThunder(1 - mTransitionFactor, + elapsedSeconds, + isPaused); + flash = currentFlash + nextFlash; + } + osg::Vec4f flashColor(flash, flash, flash, 0.0f); - mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); - mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); - mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); + mResult.mFogColor += flashColor; + mResult.mAmbientColor += flashColor; + mResult.mSunColor += flashColor; + } - mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); - mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); - mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); - mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + inline void WeatherManager::calculateResult(const int weatherID, const float gameHour) + { + const Weather& current = mWeatherSettings[weatherID]; - mResult.mNight = current.mNight; + mResult.mCloudTexture = current.mCloudTexture; + mResult.mCloudBlendFactor = 0; + mResult.mNextWindSpeed = 0; + mResult.mWindSpeed = mResult.mCurrentWindSpeed = calculateWindSpeed(weatherID, mWindSpeed); + mResult.mBaseWindSpeed = mWeatherSettings[weatherID].mWindSpeed; - float threshold = mWeatherSettings[mNextWeather].mRainThreshold; - if (threshold <= 0) - threshold = 0.5f; + mResult.mCloudSpeed = current.mCloudSpeed; + mResult.mGlareView = current.mGlareView; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mAmbientSoundVolume = 1.f; + mResult.mPrecipitationAlpha = 1.f; - if(factor < threshold) - { mResult.mIsStorm = current.mIsStorm; - mResult.mParticleEffect = current.mParticleEffect; - mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = 1 - factor / threshold; - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; mResult.mRainDiameter = current.mRainDiameter; mResult.mRainMinHeight = current.mRainMinHeight; mResult.mRainMaxHeight = current.mRainMaxHeight; mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); + + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); + mResult.mDLFogFactor = current.mDL.FogFactor; + mResult.mDLFogOffset = current.mDL.FogOffset; + + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) + { + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; + factor = std::min(1.f, factor); + mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); + // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because + // MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline + // would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped. + // Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense. + mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor); + for (int i=0; i<3; ++i) + mResult.mSunDiscColor[i] = std::min(1.f, mResult.mSunDiscColor[i]); + } + else + mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); + + if (gameHour >= mTimeSettings.mDayEnd) + { + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); + fade = fade*fade; + mResult.mSunDiscColor.a() = 1.f - fade; + } + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) + { + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; + } + else + mResult.mSunDiscColor.a() = 1; + + mResult.mStormDirection = calculateStormDirection(mResult.mParticleEffect); } - else + + inline void WeatherManager::calculateTransitionResult(const float factor, const float gameHour) { - mResult.mIsStorm = other.mIsStorm; - mResult.mParticleEffect = other.mParticleEffect; - mResult.mRainEffect = other.mRainEffect; - mResult.mRainSpeed = other.mRainSpeed; - mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; - mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); - mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; - mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; - - mResult.mRainDiameter = other.mRainDiameter; - mResult.mRainMinHeight = other.mRainMinHeight; - mResult.mRainMaxHeight = other.mRainMaxHeight; - mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + calculateResult(mCurrentWeather, gameHour); + const MWRender::WeatherResult current = mResult; + calculateResult(mNextWeather, gameHour); + const MWRender::WeatherResult other = mResult; + + mResult.mStormDirection = current.mStormDirection; + mResult.mNextStormDirection = other.mStormDirection; + + mResult.mCloudTexture = current.mCloudTexture; + mResult.mNextCloudTexture = other.mCloudTexture; + mResult.mCloudBlendFactor = mWeatherSettings[mNextWeather].cloudBlendFactor(factor); + + mResult.mFogColor = lerp(current.mFogColor, other.mFogColor, factor); + mResult.mSunColor = lerp(current.mSunColor, other.mSunColor, factor); + mResult.mSkyColor = lerp(current.mSkyColor, other.mSkyColor, factor); + + mResult.mAmbientColor = lerp(current.mAmbientColor, other.mAmbientColor, factor); + mResult.mSunDiscColor = lerp(current.mSunDiscColor, other.mSunDiscColor, factor); + mResult.mFogDepth = lerp(current.mFogDepth, other.mFogDepth, factor); + mResult.mDLFogFactor = lerp(current.mDLFogFactor, other.mDLFogFactor, factor); + mResult.mDLFogOffset = lerp(current.mDLFogOffset, other.mDLFogOffset, factor); + + mResult.mCurrentWindSpeed = calculateWindSpeed(mCurrentWeather, mCurrentWindSpeed); + mResult.mNextWindSpeed = calculateWindSpeed(mNextWeather, mNextWindSpeed); + mResult.mBaseWindSpeed = lerp(current.mBaseWindSpeed, other.mBaseWindSpeed, factor); + + mResult.mWindSpeed = lerp(mResult.mCurrentWindSpeed, mResult.mNextWindSpeed, factor); + mResult.mCloudSpeed = lerp(current.mCloudSpeed, other.mCloudSpeed, factor); + mResult.mGlareView = lerp(current.mGlareView, other.mGlareView, factor); + mResult.mNightFade = lerp(current.mNightFade, other.mNightFade, factor); + + mResult.mNight = current.mNight; + + float threshold = mWeatherSettings[mNextWeather].mRainThreshold; + if (threshold <= 0.f) + threshold = 0.5f; + + if(factor < threshold) + { + mResult.mIsStorm = current.mIsStorm; + mResult.mParticleEffect = current.mParticleEffect; + mResult.mRainEffect = current.mRainEffect; + mResult.mRainSpeed = current.mRainSpeed; + mResult.mRainEntranceSpeed = current.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = 1.f - factor / threshold; + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = current.mAmbientLoopSoundID; + mResult.mRainDiameter = current.mRainDiameter; + mResult.mRainMinHeight = current.mRainMinHeight; + mResult.mRainMaxHeight = current.mRainMaxHeight; + mResult.mRainMaxRaindrops = current.mRainMaxRaindrops; + } + else + { + mResult.mIsStorm = other.mIsStorm; + mResult.mParticleEffect = other.mParticleEffect; + mResult.mRainEffect = other.mRainEffect; + mResult.mRainSpeed = other.mRainSpeed; + mResult.mRainEntranceSpeed = other.mRainEntranceSpeed; + mResult.mAmbientSoundVolume = (factor - threshold) / (1 - threshold); + mResult.mPrecipitationAlpha = mResult.mAmbientSoundVolume; + mResult.mAmbientLoopSoundID = other.mAmbientLoopSoundID; + + mResult.mRainDiameter = other.mRainDiameter; + mResult.mRainMinHeight = other.mRainMinHeight; + mResult.mRainMaxHeight = other.mRainMaxHeight; + mResult.mRainMaxRaindrops = other.mRainMaxRaindrops; + } } } diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index a3928465c4..21b2fae9f8 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -115,6 +115,8 @@ namespace MWWorld class Weather { public: + static osg::Vec3f defaultDirection(); + Weather(const std::string& name, float stormWindSpeed, float rainSpeed, @@ -189,6 +191,8 @@ namespace MWWorld std::string mRainEffect; + osg::Vec3f mStormDirection; + // Note: For Weather Blight, there is a "Disease Chance" (=0.1) setting. But according to MWSFD this feature // is broken in the vanilla game and was disabled. diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 299cc8676b..f0ac894cfc 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -7,6 +7,8 @@ #include #include +#include + #include #include @@ -27,10 +29,10 @@ #include -#include -#include -#include -#include +#include +#include + +#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" @@ -81,34 +83,42 @@ namespace MWWorld { struct GameContentLoader : public ContentLoader { - GameContentLoader(Loading::Listener& listener) - : ContentLoader(listener) - { - } - - bool addLoader(const std::string& extension, ContentLoader* loader) + void addLoader(std::string&& extension, ContentLoader& loader) { - return mLoaders.insert(std::make_pair(extension, loader)).second; + mLoaders.emplace(std::move(extension), &loader); } - void load(const boost::filesystem::path& filepath, int& index) override + void load(const boost::filesystem::path& filepath, int& index, Loading::Listener* listener) override { - LoadersContainer::iterator it(mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string()))); + const auto it = mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string())); if (it != mLoaders.end()) { - it->second->load(filepath, index); + const std::string filename = filepath.filename().string(); + Log(Debug::Info) << "Loading content file " << filename; + if (listener != nullptr) + listener->setLabel(MyGUI::TextIterator::toTagsString(filename)); + it->second->load(filepath, index, listener); } else { - std::string msg("Cannot load file: "); - msg += filepath.string(); - throw std::runtime_error(msg.c_str()); + std::string msg("Cannot load file: "); + msg += filepath.string(); + throw std::runtime_error(msg.c_str()); } } private: - typedef std::map LoadersContainer; - LoadersContainer mLoaders; + std::map mLoaders; + }; + + struct OMWScriptsLoader : public ContentLoader + { + ESMStore& mStore; + OMWScriptsLoader(ESMStore& store) : mStore(store) {} + void load(const boost::filesystem::path& filepath, int& /*index*/, Loading::Listener* /*listener*/) override + { + mStore.addOMWScripts(filepath.string()); + } }; void World::adjustSky() @@ -135,32 +145,32 @@ namespace MWWorld : mResourceSystem(resourceSystem), mLocalScripts (mStore), mCells (mStore, mEsm), mSky (true), mGodMode(false), mScriptsEnabled(true), mDiscardMovements(true), mContentFiles (contentFiles), - mUserDataPath(userDataPath), mShouldUpdateNavigator(false), + mUserDataPath(userDataPath), + mDefaultHalfExtents(Settings::Manager::getVector3("default actor pathfind half extents", "Game")), + mShouldUpdateNavigator(false), mActivationDistanceOverride (activationDistanceOverride), mStartCell(startCell), mDistanceToFacedObject(-1.f), mTeleportEnabled(true), mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f) { - mEsm.resize(contentFiles.size() + groundcoverFiles.size()); + mEsm.resize(contentFiles.size()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener->loadingOn(); - GameContentLoader gameContentLoader(*listener); - EsmLoader esmLoader(mStore, mEsm, encoder, *listener); - - gameContentLoader.addLoader(".esm", &esmLoader); - gameContentLoader.addLoader(".esp", &esmLoader); - gameContentLoader.addLoader(".omwgame", &esmLoader); - gameContentLoader.addLoader(".omwaddon", &esmLoader); - gameContentLoader.addLoader(".project", &esmLoader); - - loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader); + loadContentFiles(fileCollections, contentFiles, mStore, mEsm, encoder, listener); + loadGroundcoverFiles(fileCollections, groundcoverFiles, encoder); listener->loadingOff(); - // insert records that may not be present in all versions of MW - if (mEsm[0].getFormat() == 0) - ensureNeededRecords(); + // Find main game file + for (const ESM::ESMReader& reader : mEsm) + { + if (!Misc::StringUtils::ciEndsWith(reader.getName(), ".esm") && !Misc::StringUtils::ciEndsWith(reader.getName(), ".omwgame")) + continue; + if (reader.getFormat() == 0) + ensureNeededRecords(); // and insert records that may not be present in all versions of MW. + break; + } mCurrentDate.reset(new DateTimeManager()); @@ -173,20 +183,18 @@ namespace MWWorld mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode)); - if (auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager()) + if (Settings::Manager::getBool("enable", "Navigator")) { - navigatorSettings->mMaxClimb = MWPhysics::sStepSizeUp; - navigatorSettings->mMaxSlope = MWPhysics::sMaxSlope; - navigatorSettings->mSwimHeightScale = mSwimHeightScale; - DetourNavigator::RecastGlobalAllocator::init(); - mNavigator.reset(new DetourNavigator::NavigatorImpl(*navigatorSettings)); + auto navigatorSettings = DetourNavigator::makeSettingsFromSettingsManager(); + navigatorSettings.mSwimHeightScale = mSwimHeightScale; + mNavigator = DetourNavigator::makeNavigator(navigatorSettings); } else { - mNavigator.reset(new DetourNavigator::NavigatorStub()); + mNavigator = DetourNavigator::makeNavigatorStub(); } - mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator)); + mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator, mGroundcoverStore)); mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get())); mRendering->preloadCommonAssets(); @@ -392,6 +400,23 @@ namespace MWWorld } } + void World::validateMasterFiles(const std::vector& readers) + { + for (const auto& esm : readers) + { + assert(esm.getGameFiles().size() == esm.getParentFileIndices().size()); + for (unsigned int i=0; i gmst; @@ -573,13 +598,7 @@ namespace MWWorld void World::useDeathCamera() { - if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() ) - { - mRendering->getCamera()->togglePreviewMode(false); - mRendering->getCamera()->toggleVanityMode(false); - } - if(mRendering->getCamera()->isFirstPerson()) - mRendering->getCamera()->toggleViewMode(true); + mRendering->getCamera()->setMode(MWRender::Camera::Mode::ThirdPerson); } MWWorld::Player& World::getPlayer() @@ -784,9 +803,9 @@ namespace MWWorld void World::addContainerScripts(const Ptr& reference, CellStore * cell) { - if( reference.getTypeName()==typeid (ESM::Container).name() || - reference.getTypeName()==typeid (ESM::NPC).name() || - reference.getTypeName()==typeid (ESM::Creature).name()) + if( reference.getType()==ESM::Container::sRecordId || + reference.getType()==ESM::NPC::sRecordId || + reference.getType()==ESM::Creature::sRecordId) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) @@ -827,9 +846,9 @@ namespace MWWorld void World::removeContainerScripts(const Ptr& reference) { - if( reference.getTypeName()==typeid (ESM::Container).name() || - reference.getTypeName()==typeid (ESM::NPC).name() || - reference.getTypeName()==typeid (ESM::Creature).name()) + if( reference.getType()==ESM::Container::sRecordId || + reference.getType()==ESM::NPC::sRecordId || + reference.getType()==ESM::Creature::sRecordId) { MWWorld::ContainerStore& container = reference.getClass().getContainerStore(reference); for(MWWorld::ContainerStoreIterator it = container.begin(); it != container.end(); ++it) @@ -1243,14 +1262,14 @@ namespace MWWorld return moveObject(ptr, cell, position, movePhysics); } - MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) { auto* actor = mPhysics->getActor(ptr); osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) - actor->adjustPosition(vec, ignoreCollisions); + actor->adjustPosition(vec); if (ptr.getClass().isActor()) - return moveObject(ptr, newpos, false, moveToActive && ptr != getPlayerPtr()); + return moveObject(ptr, newpos, false, ptr != getPlayerPtr()); return moveObject(ptr, newpos); } @@ -1298,7 +1317,7 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - objRot[0] = osg::clampBetween(objRot[0], -osg::PIf / 2, osg::PIf / 2); + objRot[0] = std::clamp(objRot[0], -osg::PI_2, osg::PI_2); objRot[1] = Misc::normalizeAngle(objRot[1]); objRot[2] = Misc::normalizeAngle(objRot[2]); } @@ -1836,7 +1855,7 @@ namespace MWWorld } bool isWerewolf = player.getClass().getNpcStats(player).isWerewolf(); - bool isFirstPerson = mRendering->getCamera()->isFirstPerson(); + bool isFirstPerson = this->isFirstPerson(); if (isWerewolf && isFirstPerson) { float werewolfFov = Fallback::Map::getFloat("General_Werewolf_FOV"); @@ -1865,7 +1884,7 @@ namespace MWWorld const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); if (!mGodMode) blind = static_cast(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude()); - MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind))); + MWBase::Environment::get().getWindowManager()->setBlindness(std::clamp(blind, 0, 100)); int nightEye = static_cast(magicEffects.get(ESM::MagicEffect::NightEye).getMagnitude()); mRendering->setNightEyeFactor(std::min(1.f, (nightEye/100.f))); @@ -1906,11 +1925,12 @@ namespace MWWorld void World::updateSoundListener() { + osg::Vec3f cameraPosition = mRendering->getCamera()->getPosition(); const ESM::Position& refpos = getPlayerPtr().getRefData().getPosition(); osg::Vec3f listenerPos; if (isFirstPerson()) - listenerPos = mRendering->getCameraPosition(); + listenerPos = cameraPosition; else listenerPos = refpos.asVec3() + osg::Vec3f(0, 0, 1.85f * mPhysics->getHalfExtents(getPlayerPtr()).z()); @@ -1921,7 +1941,7 @@ namespace MWWorld osg::Vec3f forward = listenerOrient * osg::Vec3f(0,1,0); osg::Vec3f up = listenerOrient * osg::Vec3f(0,0,1); - bool underwater = isUnderwater(getPlayerPtr().getCell(), mRendering->getCameraPosition()); + bool underwater = isUnderwater(getPlayerPtr().getCell(), cameraPosition); MWBase::Environment::get().getSoundManager()->setListenerPosDir(listenerPos, forward, up, underwater); } @@ -2373,7 +2393,7 @@ namespace MWWorld bool World::isFirstPerson() const { - return mRendering->getCamera()->isFirstPerson(); + return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::FirstPerson; } bool World::isPreviewModeEnabled() const @@ -2381,11 +2401,6 @@ namespace MWWorld return mRendering->getCamera()->getMode() == MWRender::Camera::Mode::Preview; } - void World::togglePreviewMode(bool enable) - { - mRendering->getCamera()->togglePreviewMode(enable); - } - bool World::toggleVanityMode(bool enable) { return mRendering->getCamera()->toggleVanityMode(enable); @@ -2401,25 +2416,19 @@ namespace MWWorld mRendering->getCamera()->applyDeferredPreviewRotationToPlayer(dt); } - void World::allowVanityMode(bool allow) - { - mRendering->getCamera()->allowVanityMode(allow); - } + MWRender::Camera* World::getCamera() { return mRendering->getCamera(); } bool World::vanityRotateCamera(float * rot) { - if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled()) + auto* camera = mRendering->getCamera(); + if(!camera->isVanityOrPreviewModeEnabled()) return false; - mRendering->getCamera()->rotateCamera(rot[0], rot[2], true); + camera->setPitch(camera->getPitch() + rot[0]); + camera->setYaw(camera->getYaw() + rot[2]); return true; } - void World::adjustCameraDistance(float dist) - { - mRendering->getCamera()->adjustCameraDistance(dist); - } - void World::saveLoaded() { mStore.validateDynamic(); @@ -2471,7 +2480,6 @@ namespace MWWorld applyLoopingParticles(player); - mDefaultHalfExtents = mPhysics->getOriginalHalfExtents(getPlayerPtr()); mNavigator->addAgent(getPathfindingHalfExtents(getPlayerConstPtr())); } @@ -2927,9 +2935,21 @@ namespace MWWorld return mScriptsEnabled; } - void World::loadContentFiles(const Files::Collections& fileCollections, - const std::vector& content, const std::vector& groundcover, ContentLoader& contentLoader) + void World::loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, ESMStore& store, std::vector& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener) { + GameContentLoader gameContentLoader; + EsmLoader esmLoader(store, readers, encoder); + validateMasterFiles(readers); + + gameContentLoader.addLoader(".esm", esmLoader); + gameContentLoader.addLoader(".esp", esmLoader); + gameContentLoader.addLoader(".omwgame", esmLoader); + gameContentLoader.addLoader(".omwaddon", esmLoader); + gameContentLoader.addLoader(".project", esmLoader); + + OMWScriptsLoader omwScriptsLoader(store); + gameContentLoader.addLoader(".omwscripts", omwScriptsLoader); + int idx = 0; for (const std::string &file : content) { @@ -2937,7 +2957,7 @@ namespace MWWorld const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string()); if (col.doesExist(file)) { - contentLoader.load(col.getPath(file), idx); + gameContentLoader.load(col.getPath(file), idx, listener); } else { @@ -2946,24 +2966,15 @@ namespace MWWorld } idx++; } + } - ESM::GroundcoverIndex = idx; + void World::loadGroundcoverFiles(const Files::Collections& fileCollections, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder) + { + if (!Settings::Manager::getBool("enabled", "Groundcover")) return; - for (const std::string &file : groundcover) - { - boost::filesystem::path filename(file); - const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string()); - if (col.doesExist(file)) - { - contentLoader.load(col.getPath(file), idx); - } - else - { - std::string message = "Failed loading " + file + ": the groundcover file does not exist"; - throw std::runtime_error(message); - } - idx++; - } + Log(Debug::Info) << "Loading groundcover:"; + + mGroundcoverStore.init(mStore.get(), fileCollections, groundcoverFiles, encoder); } bool World::startSpellCast(const Ptr &actor) @@ -3415,12 +3426,34 @@ namespace MWWorld return true; // Consider references inside containers as well (except if we are looking for a Creature, they cannot be in containers) - bool isContainer = ptr.getClass().getTypeName() == typeid(ESM::Container).name(); + bool isContainer = ptr.getClass().getType() == ESM::Container::sRecordId; if (mType != World::Detect_Creature && (ptr.getClass().isActor() || isContainer)) { // but ignore containers without resolved content if (isContainer && ptr.getRefData().getCustomData() == nullptr) + { + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + for(const auto& containerItem : ptr.get()->mBase->mInventory.mList) + { + if(containerItem.mCount) + { + try + { + ManualRef ref(store, containerItem.mItem, containerItem.mCount); + if(needToAdd(ref.getPtr(), mDetector)) + { + mOut.push_back(ptr); + return true; + } + } + catch (const std::exception&) + { + // Ignore invalid item id + } + } + } return true; + } MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); { @@ -3448,10 +3481,10 @@ namespace MWWorld // If in werewolf form, this detects only NPCs, otherwise only creatures if (detector.getClass().isNpc() && detector.getClass().getNpcStats(detector).isWerewolf()) { - if (ptr.getClass().getTypeName() != typeid(ESM::NPC).name()) + if (ptr.getClass().getType() != ESM::NPC::sRecordId) return false; } - else if (ptr.getClass().getTypeName() != typeid(ESM::Creature).name()) + else if (ptr.getClass().getType() != ESM::Creature::sRecordId) return false; if (ptr.getClass().getCreatureStats(ptr).isDead()) @@ -3769,7 +3802,7 @@ namespace MWWorld cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; - cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); + cast.inflict(applyPair.first, caster, effectsToApply, rangeType, true); } } @@ -3912,7 +3945,7 @@ namespace MWWorld btVector3 aabbMin; btVector3 aabbMax; - object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->mCollisionShape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto toLocal = object->getTransform().inverse(); const auto localFrom = toLocal(Misc::Convert::toBullet(position)); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 6e48f045c0..84d97ce37c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -15,6 +15,7 @@ #include "timestamp.hpp" #include "globals.hpp" #include "contentloader.hpp" +#include "groundcoverstore.hpp" namespace osg { @@ -80,6 +81,7 @@ namespace MWWorld std::vector mEsm; MWWorld::ESMStore mStore; + GroundcoverStore mGroundcoverStore; LocalScripts mLocalScripts; MWWorld::Globals mGlobalVariables; @@ -157,19 +159,15 @@ namespace MWWorld void updateNavigatorObject(const MWPhysics::Object& object); void ensureNeededRecords(); + void validateMasterFiles(const std::vector& readers); void fillGlobalVariables(); void updateSkyDate(); - /** - * @brief loadContentFiles - Loads content files (esm,esp,omwgame,omwaddon) - * @param fileCollections- Container which holds content file names and their paths - * @param content - Container which holds content file names - * @param contentLoader - - */ - void loadContentFiles(const Files::Collections& fileCollections, - const std::vector& content, const std::vector& groundcover, ContentLoader& contentLoader); + void loadContentFiles(const Files::Collections& fileCollections, const std::vector& content, ESMStore& store, std::vector& readers, ToUTF8::Utf8Encoder* encoder, Loading::Listener* listener); + + void loadGroundcoverFiles(const Files::Collections& fileCollections, const std::vector& groundcoverFiles, ToUTF8::Utf8Encoder* encoder); float feetToGameUnits(float feet); float getActivationDistancePlusTelekinesis(); @@ -376,7 +374,7 @@ namespace MWWorld MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr - MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; @@ -531,13 +529,10 @@ namespace MWWorld bool isFirstPerson() const override; bool isPreviewModeEnabled() const override; - void togglePreviewMode(bool enable) override; - bool toggleVanityMode(bool enable) override; - void allowVanityMode(bool allow) override; + MWRender::Camera* getCamera() override; bool vanityRotateCamera(float * rot) override; - void adjustCameraDistance(float dist) override; void applyDeferredPreviewRotationToPlayer(float dt) override; void disableDeferredPreviewRotation() override; diff --git a/apps/openmw/options.cpp b/apps/openmw/options.cpp index f7d20b4dd7..bde10755ea 100644 --- a/apps/openmw/options.cpp +++ b/apps/openmw/options.cpp @@ -41,14 +41,11 @@ namespace OpenMW "set initial cell") ("content", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") ("groundcover", bpo::value()->default_value(StringsVector(), "") ->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon") - ("lua-scripts", bpo::value()->default_value(StringsVector(), "") - ->multitoken()->composing(), "file(s) with a list of global Lua scripts: omwscripts") - ("no-sound", bpo::value()->implicit_value(true) ->default_value(false), "disable all sounds") diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 5665f59508..7cfac20421 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -12,6 +12,8 @@ if (GTEST_FOUND AND GMOCK_FOUND) mwdialogue/test_keywordsearch.cpp + mwscript/test_scripts.cpp + esm/test_fixed_string.cpp esm/variant.cpp @@ -20,10 +22,15 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_utilpackage.cpp lua/test_serialization.cpp lua/test_querypackage.cpp - lua/test_omwscriptsparser.cpp + lua/test_configuration.cpp + + lua/test_ui_content.cpp misc/test_stringops.cpp misc/test_endianness.cpp + misc/test_resourcehelpers.cpp + misc/progressreporter.cpp + misc/compression.cpp nifloader/testbulletnifloader.cpp @@ -34,6 +41,10 @@ if (GTEST_FOUND AND GMOCK_FOUND) detournavigator/recastmeshobject.cpp detournavigator/navmeshtilescache.cpp detournavigator/tilecachedrecastmeshmanager.cpp + detournavigator/serialization/binaryreader.cpp + detournavigator/serialization/binarywriter.cpp + detournavigator/serialization/sizeaccumulator.cpp + detournavigator/serialization/integration.cpp settings/parser.cpp @@ -43,13 +54,23 @@ if (GTEST_FOUND AND GMOCK_FOUND) ../openmw/options.cpp openmw/options.cpp + + sqlite3/db.cpp + sqlite3/request.cpp + sqlite3/statement.cpp + sqlite3/transaction.cpp + + esmloader/load.cpp + esmloader/esmdata.cpp + + files/hash.cpp ) source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES}) - target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components ${LUA_LIBRARIES}) + target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components) # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT}) @@ -68,4 +89,12 @@ if (GTEST_FOUND AND GMOCK_FOUND) endif (CMAKE_CL_64) endif (MSVC) + file(DOWNLOAD + https://gitlab.com/OpenMW/example-suite/-/raw/8966dab24692555eec720c854fb0f73d108070cd/data/template.omwgame + ${CMAKE_CURRENT_BINARY_DIR}/data/template.omwgame + EXPECTED_MD5 bf3691034a38611534c74c3b89a7d2c3 + ) + + target_compile_definitions(openmw_test_suite PRIVATE OPENMW_DATA_DIR="${CMAKE_CURRENT_BINARY_DIR}/data") + endif() diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index 62d3f9de04..4e2b7758ff 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include #include #include +#include #include @@ -34,8 +36,8 @@ namespace { Settings mSettings; std::unique_ptr mNavigator; - osg::Vec3f mPlayerPosition; - osg::Vec3f mAgentHalfExtents; + const osg::Vec3f mPlayerPosition; + const osg::Vec3f mAgentHalfExtents; osg::Vec3f mStart; osg::Vec3f mEnd; std::deque mPath; @@ -45,14 +47,14 @@ namespace Loading::Listener mListener; const osg::Vec2i mCellPosition {0, 0}; const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); - const osg::Vec3f mShift {0, 0, 0}; const float mEndTolerance = 0; + const btTransform mTransform {btMatrix3x3::getIdentity(), btVector3(256, 256, 0)}; DetourNavigatorNavigatorTest() - : mPlayerPosition(0, 0, 0) + : mPlayerPosition(256, 256, 0) , mAgentHalfExtents(29, 29, 66) - , mStart(-204, 204, 1) - , mEnd(204, -204, 1) + , mStart(52, 460, 1) + , mEnd(460, 52, 1) , mOut(mPath) , mStepSize(28.333332061767578125f) { @@ -117,7 +119,7 @@ namespace osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) { osg::ref_ptr bulletShape(new Resource::BulletShape); - bulletShape->mCollisionShape = std::move(shape).release(); + bulletShape->mCollisionShape.reset(std::move(shape).release()); return new Resource::BulletShapeInstance(bulletShape); } @@ -134,9 +136,14 @@ namespace osg::ref_ptr mInstance; }; + btVector3 getHeightfieldShift(const osg::Vec2i& cellPosition, int cellSize, float minHeight, float maxHeight) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.x(), cellSize, minHeight, maxHeight); + } + TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -144,7 +151,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { mNavigator->addAgent(mAgentHalfExtents); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } @@ -153,7 +160,7 @@ namespace mNavigator->addAgent(mAgentHalfExtents); mNavigator->addAgent(mAgentHalfExtents); mNavigator->removeAgent(mAgentHalfExtents); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } @@ -167,38 +174,39 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204.0000152587890625, 204, 1.99998295307159423828125), - Vec3fEq(-183.96533203125, 183.9653167724609375, 1.99998819828033447265625), - Vec3fEq(-163.930633544921875, 163.9306182861328125, 1.99999344348907470703125), - Vec3fEq(-143.8959503173828125, 143.89593505859375, -2.720611572265625), - Vec3fEq(-123.86126708984375, 123.86124420166015625, -13.1089687347412109375), - Vec3fEq(-103.82657623291015625, 103.8265533447265625, -23.497333526611328125), - Vec3fEq(-83.7918853759765625, 83.7918548583984375, -33.885692596435546875), - Vec3fEq(-63.757190704345703125, 63.757171630859375, -44.274051666259765625), - Vec3fEq(-43.722503662109375, 43.72248077392578125, -54.66241455078125), - Vec3fEq(-23.687808990478515625, 23.6877918243408203125, -65.05077362060546875), - Vec3fEq(-3.6531188488006591796875, 3.6531002521514892578125, -75.43914031982421875), - Vec3fEq(16.3815746307373046875, -16.381591796875, -69.74927520751953125), - Vec3fEq(36.416263580322265625, -36.416286468505859375, -60.4739532470703125), - Vec3fEq(56.450958251953125, -56.450977325439453125, -51.1986236572265625), - Vec3fEq(76.48564910888671875, -76.4856719970703125, -41.92330169677734375), - Vec3fEq(96.5203399658203125, -96.52036285400390625, -31.46941375732421875), - Vec3fEq(116.55503082275390625, -116.5550537109375, -19.597003936767578125), - Vec3fEq(136.5897216796875, -136.5897369384765625, -7.724592685699462890625), - Vec3fEq(156.624420166015625, -156.624420166015625, 1.99999535083770751953125), - Vec3fEq(176.6591033935546875, -176.65911865234375, 1.99999010562896728515625), - Vec3fEq(196.69378662109375, -196.6938018798828125, 1.99998486042022705078125), - Vec3fEq(204, -204.0000152587890625, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -212,76 +220,77 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125), - Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375), - Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125), - Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125), - Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625), - Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375), - Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875), - Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125), - Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625), - Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125), - Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625), - Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; - mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-189.9427337646484375, 179.3997802734375, -3.622931003570556640625), - Vec3fEq(-175.8854522705078125, 154.7995452880859375, -9.24583911895751953125), - Vec3fEq(-161.82818603515625, 130.1993255615234375, -14.86874866485595703125), - Vec3fEq(-147.770904541015625, 105.5991058349609375, -20.4916591644287109375), - Vec3fEq(-133.7136383056640625, 80.99887847900390625, -26.1145648956298828125), - Vec3fEq(-119.65636444091796875, 56.39865875244140625, -31.7374725341796875), - Vec3fEq(-105.59909820556640625, 31.798435211181640625, -26.133396148681640625), - Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875), - Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625), - Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125), - Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625), - Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625), - Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625), - Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.8243885040283203125), - Vec3fEq(53.413402557373046875, -120.6742401123046875, -31.3303241729736328125), - Vec3fEq(78.20446014404296875, -134.39215087890625, -25.8431549072265625), - Vec3fEq(102.99552154541015625, -148.110076904296875, -20.3559894561767578125), - Vec3fEq(127.7865753173828125, -161.827972412109375, -14.868824005126953125), - Vec3fEq(152.57763671875, -175.5458984375, -9.3816623687744140625), - Vec3fEq(177.3686981201171875, -189.2638092041015625, -3.894496917724609375), - Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.59266507625579833984375), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125), + Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875), + Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625), + Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875), + Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -295,79 +304,80 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); - mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-189.9427337646484375, 179.3997802734375, -3.622931003570556640625), - Vec3fEq(-175.8854522705078125, 154.7995452880859375, -9.24583911895751953125), - Vec3fEq(-161.82818603515625, 130.1993255615234375, -14.86874866485595703125), - Vec3fEq(-147.770904541015625, 105.5991058349609375, -20.4916591644287109375), - Vec3fEq(-133.7136383056640625, 80.99887847900390625, -26.1145648956298828125), - Vec3fEq(-119.65636444091796875, 56.39865875244140625, -31.7374725341796875), - Vec3fEq(-105.59909820556640625, 31.798435211181640625, -26.133396148681640625), - Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875), - Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625), - Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125), - Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625), - Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625), - Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625), - Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.8243885040283203125), - Vec3fEq(53.413402557373046875, -120.6742401123046875, -31.3303241729736328125), - Vec3fEq(78.20446014404296875, -134.39215087890625, -25.8431549072265625), - Vec3fEq(102.99552154541015625, -148.110076904296875, -20.3559894561767578125), - Vec3fEq(127.7865753173828125, -161.827972412109375, -14.868824005126953125), - Vec3fEq(152.57763671875, -175.5458984375, -9.3816623687744140625), - Vec3fEq(177.3686981201171875, -189.2638092041015625, -3.894496917724609375), - Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.59266507625579833984375), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -19.575878143310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.3511219024658203125), + Vec3fEq(227.9903564453125, 170.28521728515625, -22.9776935577392578125), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -31.1692962646484375), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -22.219127655029296875), + Vec3fEq(329.774017333984375, 120.44022369384765625, -13.2701435089111328125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -5.330339908599853515625), + Vec3fEq(380.665863037109375, 95.51773834228515625, -3.5501649379730224609375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -1.76998889446258544921875), + Vec3fEq(431.557708740234375, 70.5952606201171875, 0.01018683053553104400634765625), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.79036080837249755859375), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; compound.shape().updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); - mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); + mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125), - Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375), - Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125), - Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125), - Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625), - Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375), - Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875), - Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125), - Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625), - Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125), - Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625), - Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -394,37 +404,37 @@ namespace heightfield2.shape().setLocalScaling(btVector3(128, 128, 1)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance()), btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance()), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance()), mTransform); + mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.999981403350830078125), - Vec3fEq(-183.965301513671875, 183.965301513671875, -0.428465187549591064453125), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, -2.8569104671478271484375), - Vec3fEq(-143.89593505859375, 143.89593505859375, -5.28535556793212890625), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, -7.7138004302978515625), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, -10.142246246337890625), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, -12.3704509735107421875), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, -14.354084014892578125), - Vec3fEq(-43.72247314453125, 43.72247314453125, -16.3377170562744140625), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -18.32135009765625), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -20.3049831390380859375), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -19.044734954833984375), - Vec3fEq(36.416290283203125, -36.416290283203125, -17.061100006103515625), - Vec3fEq(56.450984954833984375, -56.450984954833984375, -15.0774688720703125), - Vec3fEq(76.4856719970703125, -76.4856719970703125, -13.0938358306884765625), - Vec3fEq(96.52036285400390625, -96.52036285400390625, -11.02784252166748046875), - Vec3fEq(116.5550537109375, -116.5550537109375, -8.5993976593017578125), - Vec3fEq(136.5897369384765625, -136.5897369384765625, -6.170953273773193359375), - Vec3fEq(156.6244354248046875, -156.6244354248046875, -3.74250507354736328125), - Vec3fEq(176.6591339111328125, -176.6591339111328125, -1.314060688018798828125), - Vec3fEq(196.693817138671875, -196.693817138671875, 1.1143856048583984375), - Vec3fEq(204, -204, 1.9999811649322509765625) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.903246104717254638671875), + Vec3fEq(96.73604583740234375, 419.93060302734375, -3.8064472675323486328125), + Vec3fEq(116.770751953125, 399.89593505859375, -6.709649562835693359375), + Vec3fEq(136.8054351806640625, 379.861236572265625, -9.33333873748779296875), + Vec3fEq(156.840118408203125, 359.826568603515625, -9.33333873748779296875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -9.33333873748779296875), + Vec3fEq(196.90948486328125, 319.757171630859375, -9.33333873748779296875), + Vec3fEq(216.944183349609375, 299.722503662109375, -9.33333873748779296875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -9.33333873748779296875), + Vec3fEq(257.0135498046875, 259.65313720703125, -9.33333873748779296875), + Vec3fEq(277.048248291015625, 239.618438720703125, -9.33333873748779296875), + Vec3fEq(297.082916259765625, 219.583740234375, -9.33333873748779296875), + Vec3fEq(317.11761474609375, 199.549041748046875, -9.33333873748779296875), + Vec3fEq(337.15228271484375, 179.5143585205078125, -9.33333873748779296875), + Vec3fEq(357.186981201171875, 159.47967529296875, -9.33333873748779296875), + Vec3fEq(377.221649169921875, 139.4449920654296875, -9.33333873748779296875), + Vec3fEq(397.25634765625, 119.41030120849609375, -6.891522884368896484375), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.053897380828857421875), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.21627247333526611328125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.621352672576904296875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -438,6 +448,7 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); + const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); const std::array heightfieldData2 {{ -25, -25, -25, -25, -25, @@ -447,10 +458,11 @@ namespace -25, -25, -25, -25, -25, }}; const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); + const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - EXPECT_TRUE(mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface1.mSize - 1), mShift, surface1)); - EXPECT_FALSE(mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface2.mSize - 1), mShift, surface2)); + EXPECT_TRUE(mNavigator->addHeightfield(mCellPosition, cellSize1, surface1)); + EXPECT_FALSE(mNavigator->addHeightfield(mCellPosition, cellSize2, surface2)); } TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape) @@ -466,7 +478,7 @@ namespace }}; std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); shapePtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mCollisionShape = shapePtr.release(); + bulletShape->mCollisionShape.reset(shapePtr.release()); std::array heightfieldDataAvoid {{ -25, -25, -25, -25, -25, @@ -477,42 +489,42 @@ namespace }}; std::unique_ptr shapeAvoidPtr = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); - bulletShape->mAvoidCollisionShape = shapeAvoidPtr.release(); + bulletShape->mAvoidCollisionShape.reset(shapeAvoidPtr.release()); osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(instance->getCollisionShape()), ObjectShapes(instance), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(instance->mCollisionShape.get()), ObjectShapes(instance), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99997997283935546875), - Vec3fEq(-191.328948974609375, 178.65789794921875, -0.815807759761810302734375), - Vec3fEq(-178.65789794921875, 153.3157806396484375, -3.6315968036651611328125), - Vec3fEq(-165.986846923828125, 127.9736785888671875, -6.4473857879638671875), - Vec3fEq(-153.3157806396484375, 102.6315765380859375, -9.26317310333251953125), - Vec3fEq(-140.6447296142578125, 77.28946685791015625, -12.07896137237548828125), - Vec3fEq(-127.9736785888671875, 51.947368621826171875, -14.894748687744140625), - Vec3fEq(-115.3026275634765625, 26.6052646636962890625, -17.7105388641357421875), - Vec3fEq(-102.63158416748046875, 1.2631585597991943359375, -20.5263233184814453125), - Vec3fEq(-89.9605712890625, -24.0789661407470703125, -19.591716766357421875), - Vec3fEq(-68.54410552978515625, -42.629238128662109375, -19.847625732421875), - Vec3fEq(-47.127635955810546875, -61.17951202392578125, -20.1035366058349609375), - Vec3fEq(-25.711170196533203125, -79.72978973388671875, -20.359447479248046875), - Vec3fEq(-4.294706821441650390625, -98.280059814453125, -20.6153545379638671875), - Vec3fEq(17.121753692626953125, -116.83034515380859375, -17.3710460662841796875), - Vec3fEq(42.7990570068359375, -128.80755615234375, -14.7094440460205078125), - Vec3fEq(68.4763641357421875, -140.7847747802734375, -12.0478420257568359375), - Vec3fEq(94.15366363525390625, -152.761993408203125, -9.3862361907958984375), - Vec3fEq(119.83097076416015625, -164.7392120361328125, -6.724635601043701171875), - Vec3fEq(145.508270263671875, -176.7164306640625, -4.06303119659423828125), - Vec3fEq(171.185577392578125, -188.69366455078125, -1.40142619609832763671875), - Vec3fEq(196.862884521484375, -200.6708831787109375, 1.2601754665374755859375), - Vec3fEq(204, -204, 1.999979496002197265625) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.013885498046875, 434.49853515625, -0.74384129047393798828125), + Vec3fEq(81.36110687255859375, 408.997100830078125, -3.4876689910888671875), + Vec3fEq(93.7083282470703125, 383.495635986328125, -6.2314929962158203125), + Vec3fEq(106.0555419921875, 357.99420166015625, -8.97531890869140625), + Vec3fEq(118.40276336669921875, 332.49273681640625, -11.7191448211669921875), + Vec3fEq(130.7499847412109375, 306.991302490234375, -14.4629726409912109375), + Vec3fEq(143.0972137451171875, 281.4898681640625, -17.206798553466796875), + Vec3fEq(155.4444122314453125, 255.9884033203125, -19.9506206512451171875), + Vec3fEq(167.7916412353515625, 230.4869537353515625, -19.91887664794921875), + Vec3fEq(189.053619384765625, 211.75982666015625, -20.1138629913330078125), + Vec3fEq(210.3155975341796875, 193.032684326171875, -20.3088512420654296875), + Vec3fEq(231.577606201171875, 174.3055419921875, -20.503841400146484375), + Vec3fEq(252.839599609375, 155.5784149169921875, -19.9803981781005859375), + Vec3fEq(278.407989501953125, 143.3704071044921875, -17.2675113677978515625), + Vec3fEq(303.976348876953125, 131.16241455078125, -14.55462360382080078125), + Vec3fEq(329.54473876953125, 118.9544219970703125, -11.84173583984375), + Vec3fEq(355.11309814453125, 106.74642181396484375, -9.12884807586669921875), + Vec3fEq(380.681488037109375, 94.538421630859375, -6.4159603118896484375), + Vec3fEq(406.249847412109375, 82.33042144775390625, -3.7030735015869140625), + Vec3fEq(431.8182373046875, 70.1224365234375, -0.990187108516693115234375), + Vec3fEq(457.38665771484375, 57.9144439697265625, 1.72269880771636962890625), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -526,38 +538,39 @@ namespace 0, -50, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, 300)); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addWater(mCellPosition, cellSize, 300); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; + mStart.x() = 256; mStart.z() = 300; - mEnd.x() = 0; + mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(0, 204, 185.33331298828125), - Vec3fEq(0, 175.6666717529296875, 185.33331298828125), - Vec3fEq(0, 147.3333282470703125, 185.33331298828125), - Vec3fEq(0, 119, 185.33331298828125), - Vec3fEq(0, 90.6666717529296875, 185.33331298828125), - Vec3fEq(0, 62.333339691162109375, 185.33331298828125), - Vec3fEq(0, 34.00000762939453125, 185.33331298828125), - Vec3fEq(0, 5.66667461395263671875, 185.33331298828125), - Vec3fEq(0, -22.6666584014892578125, 185.33331298828125), - Vec3fEq(0, -50.999988555908203125, 185.33331298828125), - Vec3fEq(0, -79.33332061767578125, 185.33331298828125), - Vec3fEq(0, -107.666656494140625, 185.33331298828125), - Vec3fEq(0, -135.9999847412109375, 185.33331298828125), - Vec3fEq(0, -164.33331298828125, 185.33331298828125), - Vec3fEq(0, -192.666656494140625, 185.33331298828125), - Vec3fEq(0, -204, 185.33331298828125) + Vec3fEq(256, 460, 185.33331298828125), + Vec3fEq(256, 431.666656494140625, 185.33331298828125), + Vec3fEq(256, 403.33331298828125, 185.33331298828125), + Vec3fEq(256, 375, 185.33331298828125), + Vec3fEq(256, 346.666656494140625, 185.33331298828125), + Vec3fEq(256, 318.33331298828125, 185.33331298828125), + Vec3fEq(256, 290, 185.33331298828125), + Vec3fEq(256, 261.666656494140625, 185.33331298828125), + Vec3fEq(256, 233.3333282470703125, 185.33331298828125), + Vec3fEq(256, 205, 185.33331298828125), + Vec3fEq(256, 176.6666717529296875, 185.33331298828125), + Vec3fEq(256, 148.3333282470703125, 185.33331298828125), + Vec3fEq(256, 120, 185.33331298828125), + Vec3fEq(256, 91.6666717529296875, 185.33331298828125), + Vec3fEq(255.999969482421875, 63.33333587646484375, 185.33331298828125), + Vec3fEq(255.999969482421875, 56.66666412353515625, 185.33331298828125) )) << mPath; } @@ -573,36 +586,37 @@ namespace 0, 0, 0, 0, 0, 0, 0, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), osg::Vec3f(0, 0, -25)); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addWater(mCellPosition, cellSize, -25); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(0, 204, -98.000030517578125), - Vec3fEq(0, 175.6666717529296875, -108.30306243896484375), - Vec3fEq(0, 147.3333282470703125, -118.6060791015625), - Vec3fEq(0, 119, -128.90911865234375), - Vec3fEq(0, 90.6666717529296875, -139.2121429443359375), - Vec3fEq(0, 62.333339691162109375, -143.3333587646484375), - Vec3fEq(0, 34.00000762939453125, -143.3333587646484375), - Vec3fEq(0, 5.66667461395263671875, -143.3333587646484375), - Vec3fEq(0, -22.6666584014892578125, -143.3333587646484375), - Vec3fEq(0, -50.999988555908203125, -143.3333587646484375), - Vec3fEq(0, -79.33332061767578125, -143.3333587646484375), - Vec3fEq(0, -107.666656494140625, -133.0303192138671875), - Vec3fEq(0, -135.9999847412109375, -122.72728729248046875), - Vec3fEq(0, -164.33331298828125, -112.4242706298828125), - Vec3fEq(0, -192.666656494140625, -102.12123870849609375), - Vec3fEq(0, -204, -98.00002288818359375) + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) )) << mPath; } @@ -618,36 +632,37 @@ namespace 0, 0, 0, 0, 0, 0, 0, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); - mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits::max(), osg::Vec3f(0, 0, -25)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addWater(mCellPosition, std::numeric_limits::max(), -25); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(0, 204, -98.000030517578125), - Vec3fEq(0, 175.6666717529296875, -108.30306243896484375), - Vec3fEq(0, 147.3333282470703125, -118.6060791015625), - Vec3fEq(0, 119, -128.90911865234375), - Vec3fEq(0, 90.6666717529296875, -139.2121429443359375), - Vec3fEq(0, 62.333339691162109375, -143.3333587646484375), - Vec3fEq(0, 34.00000762939453125, -143.3333587646484375), - Vec3fEq(0, 5.66667461395263671875, -143.3333587646484375), - Vec3fEq(0, -22.6666584014892578125, -143.3333587646484375), - Vec3fEq(0, -50.999988555908203125, -143.3333587646484375), - Vec3fEq(0, -79.33332061767578125, -143.3333587646484375), - Vec3fEq(0, -107.666656494140625, -133.0303192138671875), - Vec3fEq(0, -135.9999847412109375, -122.72728729248046875), - Vec3fEq(0, -164.33331298828125, -112.4242706298828125), - Vec3fEq(0, -192.666656494140625, -102.12123870849609375), - Vec3fEq(0, -204, -98.00002288818359375) + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) )) << mPath; } @@ -663,37 +678,37 @@ namespace 0, 0, 0, 0, 0, 0, 0, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, -25)); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addWater(mCellPosition, cellSize, -25); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mStart.x() = 0; - mEnd.x() = 0; + mStart.x() = 256; + mEnd.x() = 256; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(0, 204, -98.000030517578125), - Vec3fEq(10.26930999755859375, 177.59320068359375, -107.4711456298828125), - Vec3fEq(20.5386199951171875, 151.1864166259765625, -116.9422607421875), - Vec3fEq(30.8079280853271484375, 124.77960968017578125, -126.41339111328125), - Vec3fEq(41.077239990234375, 98.37281036376953125, -135.8845062255859375), - Vec3fEq(51.346546173095703125, 71.96601104736328125, -138.2003936767578125), - Vec3fEq(61.615856170654296875, 45.559215545654296875, -140.0838470458984375), - Vec3fEq(71.88516998291015625, 19.1524181365966796875, -141.9673004150390625), - Vec3fEq(82.15447235107421875, -7.254379749298095703125, -142.3074798583984375), - Vec3fEq(81.04636383056640625, -35.56603240966796875, -142.7104339599609375), - Vec3fEq(79.93825531005859375, -63.877685546875, -143.1133880615234375), - Vec3fEq(78.83014678955078125, -92.18933868408203125, -138.7660675048828125), - Vec3fEq(62.50392913818359375, -115.3460235595703125, -130.237823486328125), - Vec3fEq(46.17771148681640625, -138.502716064453125, -121.8172149658203125), - Vec3fEq(29.85149383544921875, -161.6594085693359375, -113.39659881591796875), - Vec3fEq(13.52527523040771484375, -184.81610107421875, -104.97599029541015625), - Vec3fEq(0, -204, -98.00002288818359375) + Vec3fEq(256, 460, -129.4098663330078125), + Vec3fEq(256, 431.666656494140625, -129.6970062255859375), + Vec3fEq(256, 403.33331298828125, -129.6970062255859375), + Vec3fEq(256, 375, -129.4439239501953125), + Vec3fEq(256, 346.666656494140625, -129.02587890625), + Vec3fEq(256, 318.33331298828125, -128.6078338623046875), + Vec3fEq(256, 290, -128.1021728515625), + Vec3fEq(256, 261.666656494140625, -126.46875), + Vec3fEq(256, 233.3333282470703125, -119.4891357421875), + Vec3fEq(256, 205, -110.62021636962890625), + Vec3fEq(256, 176.6666717529296875, -101.7512969970703125), + Vec3fEq(256, 148.3333282470703125, -92.88237762451171875), + Vec3fEq(256, 120, -75.29378509521484375), + Vec3fEq(256, 91.6666717529296875, -55.201839447021484375), + Vec3fEq(256.000030517578125, 63.33333587646484375, -34.800380706787109375), + Vec3fEq(256.000030517578125, 56.66666412353515625, -30.00003814697265625) )) << mPath; } @@ -710,7 +725,7 @@ namespace heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -718,36 +733,36 @@ namespace mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125), - Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375), - Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125), - Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125), - Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625), - Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375), - Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875), - Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125), - Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625), - Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125), - Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625), - Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -761,9 +776,10 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -771,65 +787,67 @@ namespace mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-183.965301513671875, 183.965301513671875, 1.99998819828033447265625), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, 1.99999344348907470703125), - Vec3fEq(-143.89593505859375, 143.89593505859375, -2.7206256389617919921875), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, -13.1089839935302734375), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, -23.4973468780517578125), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, -33.885707855224609375), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, -44.27407073974609375), - Vec3fEq(-43.72247314453125, 43.72247314453125, -54.662433624267578125), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, -65.0507965087890625), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, -75.43915557861328125), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, -69.749267578125), - Vec3fEq(36.416290283203125, -36.416290283203125, -60.4739532470703125), - Vec3fEq(56.450984954833984375, -56.450984954833984375, -51.1986236572265625), - Vec3fEq(76.4856719970703125, -76.4856719970703125, -41.92330169677734375), - Vec3fEq(96.52036285400390625, -96.52036285400390625, -31.46941375732421875), - Vec3fEq(116.5550537109375, -116.5550537109375, -19.597003936767578125), - Vec3fEq(136.5897369384765625, -136.5897369384765625, -7.724592685699462890625), - Vec3fEq(156.6244354248046875, -156.6244354248046875, 1.99999535083770751953125), - Vec3fEq(176.6591339111328125, -176.6591339111328125, 1.99999010562896728515625), - Vec3fEq(196.693817138671875, -196.693817138671875, 1.99998486042022705078125), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(76.70135498046875, 439.965301513671875, -0.9659786224365234375), + Vec3fEq(96.73604583740234375, 419.93060302734375, -4.002437114715576171875), + Vec3fEq(116.770751953125, 399.89593505859375, -7.0388965606689453125), + Vec3fEq(136.8054351806640625, 379.861236572265625, -11.5593852996826171875), + Vec3fEq(156.840118408203125, 359.826568603515625, -20.7333812713623046875), + Vec3fEq(176.8748016357421875, 339.7918701171875, -34.014251708984375), + Vec3fEq(196.90948486328125, 319.757171630859375, -47.2951202392578125), + Vec3fEq(216.944183349609375, 299.722503662109375, -59.4111785888671875), + Vec3fEq(236.9788665771484375, 279.68780517578125, -65.76436614990234375), + Vec3fEq(257.0135498046875, 259.65313720703125, -68.12311553955078125), + Vec3fEq(277.048248291015625, 239.618438720703125, -66.5666656494140625), + Vec3fEq(297.082916259765625, 219.583740234375, -60.305889129638671875), + Vec3fEq(317.11761474609375, 199.549041748046875, -49.181324005126953125), + Vec3fEq(337.15228271484375, 179.5143585205078125, -35.742702484130859375), + Vec3fEq(357.186981201171875, 159.47967529296875, -22.304073333740234375), + Vec3fEq(377.221649169921875, 139.4449920654296875, -12.65070629119873046875), + Vec3fEq(397.25634765625, 119.41030120849609375, -7.41098117828369140625), + Vec3fEq(417.291046142578125, 99.3756103515625, -4.382833957672119140625), + Vec3fEq(437.325714111328125, 79.340911865234375, -1.354687213897705078125), + Vec3fEq(457.360443115234375, 59.3062286376953125, 1.624610424041748046875), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position) { - const std::array heightfieldData {{ - 0, 0, 0, 0, 0, - 0, -25, -25, -25, -25, - 0, -25, -100, -100, -100, - 0, -25, -100, -100, -100, - 0, -25, -100, -100, -100, + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, -25, + 0, -25, -1000, -1000, -100, -100, + 0, -25, -1000, -1000, -100, -100, + 0, -25, -100, -100, -100, -100, + 0, -25, -100, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); Misc::Rng::init(42); - const auto result = mNavigator->findRandomPointAroundCircle(mAgentHalfExtents, mStart, 100.0, Flag_walk); + const auto result = findRandomPointAroundCircle(*mNavigator, mAgentHalfExtents, mStart, 100.0, Flag_walk); - ASSERT_THAT(result, Optional(Vec3fEq(-198.909332275390625, 123.06096649169921875, 1.99998414516448974609375))) + ASSERT_THAT(result, Optional(Vec3fEq(69.6253509521484375, 531.29852294921875, -2.6667339801788330078125))) << (result ? *result : osg::Vec3f()); const auto distance = (*result - mStart).length(); - EXPECT_FLOAT_EQ(distance, 81.105133056640625) << distance; + EXPECT_FLOAT_EQ(distance, 73.536231994628906) << distance; } TEST_F(DetourNavigatorNavigatorTest, multiple_threads_should_lock_tiles) @@ -845,17 +863,19 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); std::vector> boxes; std::generate_n(std::back_inserter(boxes), 100, [] { return std::make_unique(btVector3(20, 20, 100)); }); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); for (std::size_t i = 0; i < boxes.size(); ++i) { - const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10, i * 10, i * 10)); + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10, shift.y() + i * 10, i * 10)); mNavigator->addObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance()), transform); } @@ -863,40 +883,40 @@ namespace for (std::size_t i = 0; i < boxes.size(); ++i) { - const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10 + 1, i * 10 + 1, i * 10 + 1)); + const btTransform transform(btMatrix3x3::getIdentity(), btVector3(shift.x() + i * 10 + 1, shift.y() + i * 10 + 1, i * 10 + 1)); mNavigator->updateObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance()), transform); } mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 1.99998295307159423828125), - Vec3fEq(-189.9427337646484375, 179.3997802734375, 1.9999866485595703125), - Vec3fEq(-175.8854522705078125, 154.7995452880859375, 1.99999034404754638671875), - Vec3fEq(-161.82818603515625, 130.1993255615234375, -3.701923847198486328125), - Vec3fEq(-147.770904541015625, 105.5991058349609375, -15.67664432525634765625), - Vec3fEq(-133.7136383056640625, 80.99887847900390625, -27.6513614654541015625), - Vec3fEq(-119.65636444091796875, 56.39865875244140625, -20.1209163665771484375), - Vec3fEq(-105.59909820556640625, 31.798435211181640625, -25.0669879913330078125), - Vec3fEq(-91.54183197021484375, 7.1982135772705078125, -31.5624217987060546875), - Vec3fEq(-77.48455810546875, -17.402008056640625, -26.98972320556640625), - Vec3fEq(-63.427295684814453125, -42.00223541259765625, -19.9045581817626953125), - Vec3fEq(-42.193531036376953125, -60.761363983154296875, -20.4544773101806640625), - Vec3fEq(-20.9597682952880859375, -79.5204925537109375, -23.599918365478515625), - Vec3fEq(3.8312885761260986328125, -93.2384033203125, -30.7141361236572265625), - Vec3fEq(28.6223468780517578125, -106.95632171630859375, -24.1782474517822265625), - Vec3fEq(53.413402557373046875, -120.6742401123046875, -19.4096889495849609375), - Vec3fEq(78.20446014404296875, -134.39215087890625, -27.6632633209228515625), - Vec3fEq(102.99552154541015625, -148.110076904296875, -15.8613681793212890625), - Vec3fEq(127.7865753173828125, -161.827972412109375, -4.059485912322998046875), - Vec3fEq(152.57763671875, -175.5458984375, 1.9999904632568359375), - Vec3fEq(177.3686981201171875, -189.2638092041015625, 1.9999866485595703125), - Vec3fEq(202.1597442626953125, -202.9817047119140625, 1.9999830722808837890625), - Vec3fEq(204, -204, 1.99998295307159423828125) + Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125), + Vec3fEq(69.5299530029296875, 434.754913330078125, -2.6775772571563720703125), + Vec3fEq(82.39324951171875, 409.50982666015625, -7.355137348175048828125), + Vec3fEq(95.25653839111328125, 384.2647705078125, -12.0326976776123046875), + Vec3fEq(108.11983489990234375, 359.019683837890625, -16.71025848388671875), + Vec3fEq(120.983123779296875, 333.774627685546875, -21.3878192901611328125), + Vec3fEq(133.8464202880859375, 308.529541015625, -26.0653781890869140625), + Vec3fEq(146.7097015380859375, 283.284454345703125, -30.7429370880126953125), + Vec3fEq(159.572998046875, 258.039398193359375, -35.420497894287109375), + Vec3fEq(172.4362945556640625, 232.7943115234375, -27.2731761932373046875), + Vec3fEq(185.2996063232421875, 207.54925537109375, -20.3612518310546875), + Vec3fEq(206.6449737548828125, 188.917236328125, -20.578319549560546875), + Vec3fEq(227.9903564453125, 170.28521728515625, -26.291717529296875), + Vec3fEq(253.4362640380859375, 157.8239593505859375, -34.784488677978515625), + Vec3fEq(278.8822021484375, 145.3627166748046875, -30.253124237060546875), + Vec3fEq(304.328094482421875, 132.9014739990234375, -25.72176361083984375), + Vec3fEq(329.774017333984375, 120.44022369384765625, -21.1904010772705078125), + Vec3fEq(355.219940185546875, 107.97898101806640625, -16.6590404510498046875), + Vec3fEq(380.665863037109375, 95.51773834228515625, -12.127681732177734375), + Vec3fEq(406.111785888671875, 83.05649566650390625, -7.5963191986083984375), + Vec3fEq(431.557708740234375, 70.5952606201171875, -3.0649592876434326171875), + Vec3fEq(457.003662109375, 58.134021759033203125, 1.4664003849029541015625), + Vec3fEq(460, 56.66666412353515625, 1.99998295307159423828125) )) << mPath; } @@ -948,15 +968,18 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - const auto result = mNavigator->raycast(mAgentHalfExtents, mStart, mEnd, Flag_walk); + const osg::Vec3f start(57, 460, 1); + const osg::Vec3f end(460, 57, 1); + const auto result = raycast(*mNavigator, mAgentHalfExtents, start, end, Flag_walk); - ASSERT_THAT(result, Optional(Vec3fEq(mEnd.x(), mEnd.y(), 1.99998295307159423828125))) + ASSERT_THAT(result, Optional(Vec3fEq(end.x(), end.y(), 1.95257937908172607421875))) << (result ? *result : osg::Vec3f()); } @@ -970,28 +993,27 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); - const btVector3 oscillatingBoxShapePosition(32, 32, 400); - CollisionShapeInstance boderBox(std::make_unique(btVector3(50, 50, 50))); + const btVector3 oscillatingBoxShapePosition(288, 288, 400); + CollisionShapeInstance borderBox(std::make_unique(btVector3(50, 50, 50))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); mNavigator->addObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance()), btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition)); // add this box to make navmesh bound box independent from oscillatingBoxShape rotations - mNavigator->addObject(ObjectId(&boderBox.shape()), ObjectShapes(boderBox.instance()), + mNavigator->addObject(ObjectId(&borderBox.shape()), ObjectShapes(borderBox.instance()), btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200))); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); + const Version expectedVersion {1, 1}; + 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); - } + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); for (int n = 0; n < 10; ++n) { @@ -1003,48 +1025,45 @@ namespace } ASSERT_EQ(navMeshes.size(), 1); - { - const auto navMesh = navMeshes.begin()->second->lockConst(); - ASSERT_EQ(navMesh->getGeneration(), 1); - ASSERT_EQ(navMesh->getNavMeshRevision(), 4); - } + ASSERT_EQ(navMeshes.begin()->second->lockConst()->getVersion(), expectedVersion); } TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) { const HeightfieldPlane plane {100}; + const int cellSize = mHeightfieldTileSize * 4; mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * 4, mShift, plane); + mNavigator->addHeightfield(mCellPosition, cellSize, plane); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, 101.99999237060546875), - Vec3fEq(-183.965301513671875, 183.965301513671875, 101.99999237060546875), - Vec3fEq(-163.9306182861328125, 163.9306182861328125, 101.99999237060546875), - Vec3fEq(-143.89593505859375, 143.89593505859375, 101.99999237060546875), - Vec3fEq(-123.86124420166015625, 123.86124420166015625, 101.99999237060546875), - Vec3fEq(-103.8265533447265625, 103.8265533447265625, 101.99999237060546875), - Vec3fEq(-83.7918548583984375, 83.7918548583984375, 101.99999237060546875), - Vec3fEq(-63.75716400146484375, 63.75716400146484375, 101.99999237060546875), - Vec3fEq(-43.72247314453125, 43.72247314453125, 101.99999237060546875), - Vec3fEq(-23.6877803802490234375, 23.6877803802490234375, 101.99999237060546875), - Vec3fEq(-3.653090000152587890625, 3.653090000152587890625, 101.99999237060546875), - Vec3fEq(16.3816013336181640625, -16.3816013336181640625, 101.99999237060546875), - Vec3fEq(36.416290283203125, -36.416290283203125, 101.99999237060546875), - Vec3fEq(56.450984954833984375, -56.450984954833984375, 101.99999237060546875), - Vec3fEq(76.4856719970703125, -76.4856719970703125, 101.99999237060546875), - Vec3fEq(96.52036285400390625, -96.52036285400390625, 101.99999237060546875), - Vec3fEq(116.5550537109375, -116.5550537109375, 101.99999237060546875), - Vec3fEq(136.5897369384765625, -136.5897369384765625, 101.99999237060546875), - Vec3fEq(156.6244354248046875, -156.6244354248046875, 101.99999237060546875), - Vec3fEq(176.6591339111328125, -176.6591339111328125, 101.99999237060546875), - Vec3fEq(196.693817138671875, -196.693817138671875, 101.99999237060546875), - Vec3fEq(204, -204, 101.99999237060546875) + Vec3fEq(56.66666412353515625, 460, 101.99999237060546875), + Vec3fEq(76.70135498046875, 439.965301513671875, 101.99999237060546875), + Vec3fEq(96.73604583740234375, 419.93060302734375, 101.99999237060546875), + Vec3fEq(116.770751953125, 399.89593505859375, 101.99999237060546875), + Vec3fEq(136.8054351806640625, 379.861236572265625, 101.99999237060546875), + Vec3fEq(156.840118408203125, 359.826568603515625, 101.99999237060546875), + Vec3fEq(176.8748016357421875, 339.7918701171875, 101.99999237060546875), + Vec3fEq(196.90948486328125, 319.757171630859375, 101.99999237060546875), + Vec3fEq(216.944183349609375, 299.722503662109375, 101.99999237060546875), + Vec3fEq(236.9788665771484375, 279.68780517578125, 101.99999237060546875), + Vec3fEq(257.0135498046875, 259.65313720703125, 101.99999237060546875), + Vec3fEq(277.048248291015625, 239.618438720703125, 101.99999237060546875), + Vec3fEq(297.082916259765625, 219.583740234375, 101.99999237060546875), + Vec3fEq(317.11761474609375, 199.549041748046875, 101.99999237060546875), + Vec3fEq(337.15228271484375, 179.5143585205078125, 101.99999237060546875), + Vec3fEq(357.186981201171875, 159.47967529296875, 101.99999237060546875), + Vec3fEq(377.221649169921875, 139.4449920654296875, 101.99999237060546875), + Vec3fEq(397.25634765625, 119.41030120849609375, 101.99999237060546875), + Vec3fEq(417.291046142578125, 99.3756103515625, 101.99999237060546875), + Vec3fEq(437.325714111328125, 79.340911865234375, 101.99999237060546875), + Vec3fEq(457.360443115234375, 59.3062286376953125, 101.99999237060546875), + Vec3fEq(460, 56.66666412353515625, 101.99999237060546875) )) << mPath; } @@ -1058,38 +1077,32 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), new btBoxShape(btVector3(200, 200, 1000))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); - mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::PartialPath); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, -2.666739940643310546875), - Vec3fEq(-193.730682373046875, 177.59320068359375, -3.9535934925079345703125), - Vec3fEq(-183.4613800048828125, 151.1864166259765625, -5.240451335906982421875), - Vec3fEq(-173.1920623779296875, 124.77962493896484375, -6.527309417724609375), - Vec3fEq(-162.922760009765625, 98.37282562255859375, -7.814167022705078125), - Vec3fEq(-152.6534423828125, 71.96602630615234375, -9.898590087890625), - Vec3fEq(-142.384124755859375, 45.559230804443359375, -17.641445159912109375), - Vec3fEq(-132.1148223876953125, 19.152431488037109375, -25.3843059539794921875), - Vec3fEq(-121.8455047607421875, -7.254369258880615234375, -27.97742462158203125), - Vec3fEq(-111.57619476318359375, -33.66117095947265625, -16.974590301513671875), - Vec3fEq(-101.30689239501953125, -60.06797027587890625, -5.9717559814453125), - Vec3fEq(-91.0375823974609375, -86.47476959228515625, -2.6667339801788330078125), - Vec3fEq(-80.76827239990234375, -112.88156890869140625, -2.6667339801788330078125), - Vec3fEq(-70.49897003173828125, -139.2883758544921875, -2.6667339801788330078125), - Vec3fEq(-60.229663848876953125, -165.6951751708984375, -2.6667339801788330078125), - Vec3fEq(-49.96035003662109375, -192.1019744873046875, -2.6667339801788330078125), - Vec3fEq(-45.333343505859375, -204, -2.6667339801788330078125) + Vec3fEq(56.66664886474609375, 460, -2.5371043682098388671875), + Vec3fEq(76.42063140869140625, 439.6884765625, -2.9134314060211181640625), + Vec3fEq(96.17461395263671875, 419.376953125, -4.50826549530029296875), + Vec3fEq(115.9285888671875, 399.0654296875, -6.1030979156494140625), + Vec3fEq(135.6825714111328125, 378.753936767578125, -7.697928905487060546875), + Vec3fEq(155.436553955078125, 358.442413330078125, -20.9574985504150390625), + Vec3fEq(175.190521240234375, 338.130889892578125, -35.907512664794921875), + Vec3fEq(194.9445037841796875, 317.8193359375, -50.85752105712890625), + Vec3fEq(214.698486328125, 297.5078125, -65.807525634765625), + Vec3fEq(222.0001068115234375, 290.000091552734375, -71.333465576171875) )) << mPath; } @@ -1103,42 +1116,42 @@ namespace 0, -25, -100, -100, -100, }}; const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)), new btBoxShape(btVector3(100, 100, 1000))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); - mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, cellSize, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), mTransform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); const float endTolerance = 1000.0f; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( - Vec3fEq(-204, 204, -2.666739940643310546875), - Vec3fEq(-188.745635986328125, 180.1236114501953125, -4.578275203704833984375), - Vec3fEq(-173.49127197265625, 156.247222900390625, -6.489814281463623046875), - Vec3fEq(-158.2369232177734375, 132.370819091796875, -8.4013538360595703125), - Vec3fEq(-142.9825592041015625, 108.49443817138671875, -10.31289386749267578125), - Vec3fEq(-127.7281951904296875, 84.6180419921875, -17.4810466766357421875), - Vec3fEq(-112.47383880615234375, 60.7416534423828125, -27.6026020050048828125), - Vec3fEq(-97.21947479248046875, 36.865261077880859375, -37.724163055419921875), - Vec3fEq(-81.965118408203125, 12.98886966705322265625, -47.84572601318359375), - Vec3fEq(-66.71075439453125, -10.887523651123046875, -46.691577911376953125), - Vec3fEq(-51.45639801025390625, -34.763916015625, -32.085445404052734375), - Vec3fEq(-36.202037811279296875, -58.640308380126953125, -28.5217914581298828125), - Vec3fEq(-20.947673797607421875, -82.5167083740234375, -32.16143035888671875), - Vec3fEq(-5.693310260772705078125, -106.393096923828125, -35.8010711669921875), - Vec3fEq(9.56105327606201171875, -130.2694854736328125, -29.6399688720703125), - Vec3fEq(24.8154163360595703125, -154.1458740234375, -17.6428318023681640625), - Vec3fEq(40.0697784423828125, -178.0222625732421875, -10.46006107330322265625), - Vec3fEq(55.3241424560546875, -201.8986663818359375, -3.297139644622802734375), - Vec3fEq(56.66666412353515625, -204, -2.6667373180389404296875) + Vec3fEq(56.66666412353515625, 460, -2.5371043682098388671875), + Vec3fEq(71.5649566650390625, 435.899810791015625, -5.817593097686767578125), + Vec3fEq(86.46324920654296875, 411.79962158203125, -9.66499996185302734375), + Vec3fEq(101.36154937744140625, 387.699462890625, -13.512401580810546875), + Vec3fEq(116.2598419189453125, 363.599273681640625, -17.359806060791015625), + Vec3fEq(131.1581268310546875, 339.499114990234375, -21.2072086334228515625), + Vec3fEq(146.056427001953125, 315.39892578125, -25.0546112060546875), + Vec3fEq(160.9547271728515625, 291.298736572265625, -28.9020137786865234375), + Vec3fEq(175.8530120849609375, 267.198577880859375, -32.749416351318359375), + Vec3fEq(190.751312255859375, 243.098388671875, -33.819454193115234375), + Vec3fEq(205.64959716796875, 218.9982147216796875, -31.020172119140625), + Vec3fEq(220.5478973388671875, 194.898040771484375, -26.844608306884765625), + Vec3fEq(235.446197509765625, 170.7978668212890625, -26.785541534423828125), + Vec3fEq(250.3444671630859375, 146.6976776123046875, -26.7264766693115234375), + Vec3fEq(265.242767333984375, 122.59751129150390625, -20.59339141845703125), + Vec3fEq(280.141021728515625, 98.4973297119140625, -14.040531158447265625), + Vec3fEq(295.039306640625, 74.39715576171875, -7.48766994476318359375), + Vec3fEq(306, 56.66666412353515625, -2.6667339801788330078125) )) << mPath; } } diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index 309266142f..c56be440e3 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -144,14 +144,14 @@ namespace const std::size_t mGeneration = 0; const std::size_t mRevision = 0; const Mesh mMesh {makeMesh()}; - const std::vector mWater {}; + const std::vector mWater {}; const std::vector mHeightfields {}; const std::vector mFlatHeightfields {}; const RecastMesh mRecastMesh {mGeneration, mRevision, mMesh, mWater, mHeightfields, mFlatHeightfields}; std::unique_ptr mPreparedNavMeshData {makePeparedNavMeshData(3)}; const std::size_t mRecastMeshSize = sizeof(mRecastMesh) + getSize(mRecastMesh); - const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(Cell); + const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(CellWater); const std::size_t mPreparedNavMeshDataSize = sizeof(*mPreparedNavMeshData) + getSize(*mPreparedNavMeshData); }; @@ -234,7 +234,7 @@ namespace { const std::size_t maxSize = 1; NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh unexistentRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); @@ -246,7 +246,7 @@ namespace const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); const auto copy = clone(*anotherPreparedNavMeshData); @@ -264,7 +264,7 @@ namespace const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); @@ -280,12 +280,12 @@ namespace NavMeshTilesCache cache(maxSize); const auto copy = clone(*mPreparedNavMeshData); - const std::vector leastRecentlySetWater {1, Cell {1, osg::Vec3f()}}; + const std::vector leastRecentlySetWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh leastRecentlySetRecastMesh {mGeneration, mRevision, mMesh, leastRecentlySetWater, mHeightfields, mFlatHeightfields}; auto leastRecentlySetData = makePeparedNavMeshData(3); - const std::vector mostRecentlySetWater {1, Cell {2, osg::Vec3f()}}; + const std::vector mostRecentlySetWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); const RecastMesh mostRecentlySetRecastMesh {mGeneration, mRevision, mMesh, mostRecentlySetWater, mHeightfields, mFlatHeightfields}; auto mostRecentlySetData = makePeparedNavMeshData(3); @@ -308,13 +308,13 @@ namespace const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector leastRecentlyUsedWater {1, Cell {1, osg::Vec3f()}}; + const std::vector leastRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh leastRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, leastRecentlyUsedWater, mHeightfields, mFlatHeightfields}; auto leastRecentlyUsedData = makePeparedNavMeshData(3); const auto leastRecentlyUsedCopy = clone(*leastRecentlyUsedData); - const std::vector mostRecentlyUsedWater {1, Cell {2, osg::Vec3f()}}; + const std::vector mostRecentlyUsedWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); const RecastMesh mostRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, mostRecentlyUsedWater, mHeightfields, mFlatHeightfields}; auto mostRecentlyUsedData = makePeparedNavMeshData(3); @@ -349,7 +349,7 @@ namespace const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; auto tooLargeData = makePeparedNavMeshData(10); @@ -364,12 +364,12 @@ namespace const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector anotherWater {1, Cell {1, osg::Vec3f()}}; + const std::vector anotherWater(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, anotherWater, mHeightfields, mFlatHeightfields}; auto anotherData = makePeparedNavMeshData(3); - const std::vector tooLargeWater {1, Cell {2, osg::Vec3f()}}; + const std::vector tooLargeWater(1, CellWater {osg::Vec2i(), Water {2, 0.0f}}); const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, tooLargeWater, mHeightfields, mFlatHeightfields}; auto tooLargeData = makePeparedNavMeshData(10); @@ -390,7 +390,7 @@ namespace const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; auto anotherData = makePeparedNavMeshData(3); @@ -409,7 +409,7 @@ namespace const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, Cell {1, osg::Vec3f()}}; + const std::vector water(1, CellWater {osg::Vec2i(), Water {1, 0.0f}}); const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; auto anotherData = makePeparedNavMeshData(3); diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/openmw_test_suite/detournavigator/operators.hpp index 92740c65f1..fb6fcc5c39 100644 --- a/apps/openmw_test_suite/detournavigator/operators.hpp +++ b/apps/openmw_test_suite/detournavigator/operators.hpp @@ -48,7 +48,7 @@ namespace testing template <> inline testing::Message& Message::operator <<(const osg::Vec3f& value) { - return (*this) << "osg::Vec3f(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() + return (*this) << "Vec3fEq(" << std::setprecision(std::numeric_limits::max_exponent10) << value.x() << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.y() << ", " << std::setprecision(std::numeric_limits::max_exponent10) << value.z() << ')'; diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp index 73d86bd6ee..ad28bf4053 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp @@ -23,9 +23,16 @@ namespace DetourNavigator { - static inline bool operator ==(const Cell& lhs, const Cell& rhs) + static inline bool operator ==(const Water& lhs, const Water& rhs) { - return lhs.mSize == rhs.mSize && lhs.mShift == rhs.mShift; + const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); }; + return tie(lhs) == tie(rhs); + } + + static inline bool operator ==(const CellWater& lhs, const CellWater& rhs) + { + const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); }; + return tie(lhs) == tie(rhs); } static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs) @@ -35,26 +42,40 @@ namespace DetourNavigator static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs) { - return std::tie(lhs.mBounds, lhs.mHeight) == std::tie(rhs.mBounds, rhs.mHeight); + const auto tie = [] (const FlatHeightfield& v) + { + return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); + }; + return tie(lhs) == tie(rhs); + } + + static inline std::ostream& operator<<(std::ostream& s, const Water& v) + { + return s << "Water {" << v.mCellSize << ", " << v.mLevel << "}"; + } + + static inline std::ostream& operator<<(std::ostream& s, const CellWater& v) + { + return s << "CellWater {" << v.mCellPosition << ", " << v.mWater << "}"; } static inline std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v) { - return s << "FlatHeightfield {" << v.mBounds << ", " << v.mHeight << "}"; + return s << "FlatHeightfield {" << v.mCellPosition << ", " << v.mCellSize << ", " << v.mHeight << "}"; } static inline std::ostream& operator<<(std::ostream& s, const Heightfield& v) { - s << "Heightfield {.mBounds=" << v.mBounds - << ", .mLength=" << int(v.mLength) + s << "Heightfield {.mCellPosition=" << v.mCellPosition + << ", .mCellSize=" << v.mCellSize + << ", .mLength=" << static_cast(v.mLength) << ", .mMinHeight=" << v.mMinHeight << ", .mMaxHeight=" << v.mMaxHeight - << ", .mShift=" << v.mShift - << ", .mScale=" << v.mScale << ", .mHeights={"; for (float h : v.mHeights) s << h << ", "; - return s << "}}"; + s << "}"; + return s << ", .mOriginalSize=" << v.mOriginalSize << "}"; } } @@ -435,10 +456,10 @@ namespace TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it) { RecastMeshBuilder builder(mBounds); - builder.addWater(1000, osg::Vec3f(100, 200, 300)); + builder.addWater(osg::Vec2i(1, 2), Water {1000, 300.0f}); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getWater(), std::vector({ - Cell {1000, osg::Vec3f(100, 200, 300)} + EXPECT_EQ(recastMesh->getWater(), std::vector({ + CellWater {osg::Vec2i(1, 2), Water {1000, 300.0f}} })); } @@ -464,62 +485,111 @@ namespace TEST_F(DetourNavigatorRecastMeshBuilderTest, add_flat_heightfield_should_add_intersection) { - mBounds.mMin = osg::Vec2f(0, 0); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float height = 10; + mBounds.mMin = osg::Vec2f(100, 100); RecastMeshBuilder builder(mBounds); - builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), 10); + builder.addHeightfield(cellPosition, cellSize, height); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); EXPECT_EQ(recastMesh->getFlatHeightfields(), std::vector({ - FlatHeightfield {TileBounds {osg::Vec2f(0, 0), osg::Vec2f(501, 502)}, 13}, + FlatHeightfield {cellPosition, cellSize, height}, })); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_inside_tile) { - constexpr std::array heights {{ + constexpr std::size_t size = 3; + constexpr std::array heights {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, }}; + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; RecastMeshBuilder builder(mBounds); - builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), heights.data(), 3, 0, 8); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); Heightfield expected; - expected.mBounds = TileBounds {osg::Vec2f(-499, -498), osg::Vec2f(501, 502)}; - expected.mLength = 3; - expected.mMinHeight = 0; - expected.mMaxHeight = 8; - expected.mShift = osg::Vec3f(-499, -498, 3); - expected.mScale = 500; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; + expected.mHeights = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }; + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_to_shifted_cell_inside_tile) + { + constexpr std::size_t size = 3; + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + const osg::Vec2i cellPosition(1, 2); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + RecastMeshBuilder builder(maxCellTileBounds(cellPosition, cellSize)); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + Heightfield expected; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; + expected.mLength = size; + expected.mMinHeight = minHeight; + expected.mMaxHeight = maxHeight; expected.mHeights = { 0, 1, 2, 3, 4, 5, 6, 7, 8, }; + expected.mOriginalSize = 3; + expected.mMinX = 0; + expected.mMinY = 0; EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_should_add_intersection) { - constexpr std::array heights {{ + constexpr std::size_t size = 3; + constexpr std::array heights {{ 0, 1, 2, 3, 4, 5, 6, 7, 8, }}; - mBounds.mMin = osg::Vec2f(250, 250); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 1000; + const float minHeight = 0; + const float maxHeight = 8; + mBounds.mMin = osg::Vec2f(750, 750); RecastMeshBuilder builder(mBounds); - builder.addHeightfield(1000, osg::Vec3f(-1, -2, 3), heights.data(), 3, 0, 8); + builder.addHeightfield(cellPosition, cellSize, heights.data(), size, minHeight, maxHeight); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); Heightfield expected; - expected.mBounds = TileBounds {osg::Vec2f(250, 250), osg::Vec2f(499, 498)}; + expected.mCellPosition = cellPosition; + expected.mCellSize = cellSize; expected.mLength = 2; expected.mMinHeight = 0; expected.mMaxHeight = 8; - expected.mShift = osg::Vec3f(-1, -2, 3); - expected.mScale = 500; expected.mHeights = { 4, 5, 7, 8, }; + expected.mOriginalSize = 3; + expected.mMinX = 1; + expected.mMinY = 1; EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); } } diff --git a/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp new file mode 100644 index 0000000000..d071326cf5 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binaryreader.cpp @@ -0,0 +1,67 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeValue) + { + std::uint32_t value = 42; + std::vector data(sizeof(value)); + std::memcpy(data.data(), &value, sizeof(value)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + binaryReader(format, result); + EXPECT_EQ(result, 42); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, shouldReadArithmeticTypeRangeValue) + { + const std::size_t count = 3; + std::vector data(sizeof(std::size_t) + count * sizeof(std::uint32_t)); + std::memcpy(data.data(), &count, sizeof(count)); + const std::uint32_t value1 = 960900021; + std::memcpy(data.data() + sizeof(count), &value1, sizeof(std::uint32_t)); + const std::uint32_t value2 = 1235496234; + std::memcpy(data.data() + sizeof(count) + sizeof(std::uint32_t), &value2, sizeof(std::uint32_t)); + const std::uint32_t value3 = 2342038092; + std::memcpy(data.data() + sizeof(count) + 2 * sizeof(std::uint32_t), &value3, sizeof(std::uint32_t)); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::size_t resultCount = 0; + const TestFormat format; + binaryReader(format, resultCount); + std::vector result(resultCount); + binaryReader(format, result.data(), result.size()); + EXPECT_THAT(result, ElementsAre(value1, value2, value3)); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeShouldThrowException) + { + std::vector data(3); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::uint32_t result = 0; + const TestFormat format; + EXPECT_THROW(binaryReader(format, result), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryReaderTest, forNotEnoughDataForArithmeticTypeRangeShouldThrowException) + { + std::vector data(7); + BinaryReader binaryReader(data.data(), data.data() + data.size()); + std::vector values(2); + const TestFormat format; + EXPECT_THROW(binaryReader(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp new file mode 100644 index 0000000000..fccc2be3da --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/binarywriter.cpp @@ -0,0 +1,57 @@ +#include "format.hpp" + +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeValue) + { + std::vector result(4); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + binaryWriter(format, std::uint32_t(42)); + EXPECT_THAT(result, ElementsAre(std::byte(42), std::byte(0), std::byte(0), std::byte(0))); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, shouldWriteArithmeticTypeRangeValue) + { + std::vector result(8); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + binaryWriter(format, values.data(), values.size()); + constexpr std::array expected { + std::byte(42), std::byte(0), std::byte(0), std::byte(0), + std::byte(13), std::byte(0), std::byte(0), std::byte(0), + }; + EXPECT_THAT(result, ElementsAreArray(expected)); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeShouldThrowException) + { + std::vector result(3); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, std::uint32_t(42)), std::runtime_error); + } + + TEST(DetourNavigatorSerializationBinaryWriterTest, forNotEnoughSpaceForArithmeticTypeRangeShouldThrowException) + { + std::vector result(7); + BinaryWriter binaryWriter(result.data(), result.data() + result.size()); + std::vector values({42, 13}); + const TestFormat format; + EXPECT_THROW(binaryWriter(format, values.data(), values.size()), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/format.hpp b/apps/openmw_test_suite/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..7c5e26a0be --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/format.hpp @@ -0,0 +1,75 @@ +#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include + +#include +#include + +namespace DetourNavigator::SerializationTesting +{ + struct Pod + { + int mInt = 42; + double mDouble = 3.14; + + friend bool operator==(const Pod& l, const Pod& r) + { + const auto tuple = [] (const Pod& v) { return std::tuple(v.mInt, v.mDouble); }; + return tuple(l) == tuple(r); + } + }; + + enum Enum + { + A, + B, + C, + }; + + struct Composite + { + short mFloatArray[3] = {0}; + std::vector mIntVector; + std::vector mEnumVector; + std::vector mPodVector; + std::size_t mPodDataSize = 0; + std::vector mPodBuffer; + std::size_t mCharDataSize = 0; + std::vector mCharBuffer; + }; + + template + struct TestFormat : Serialization::Format> + { + using Serialization::Format>::operator(); + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Pod>> + { + visitor(*this, value.mInt); + visitor(*this, value.mDouble); + } + + template + auto operator()(Visitor&& visitor, T& value) const + -> std::enable_if_t, Composite>> + { + visitor(*this, value.mFloatArray); + visitor(*this, value.mIntVector); + visitor(*this, value.mEnumVector); + visitor(*this, value.mPodVector); + visitor(*this, value.mPodDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mPodBuffer.resize(value.mPodDataSize); + visitor(*this, value.mPodBuffer.data(), value.mPodDataSize); + visitor(*this, value.mCharDataSize); + if constexpr (mode == Serialization::Mode::Read) + value.mCharBuffer.resize(value.mCharDataSize); + visitor(*this, value.mCharBuffer.data(), value.mCharDataSize); + } + }; +} + +#endif diff --git a/apps/openmw_test_suite/detournavigator/serialization/integration.cpp b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp new file mode 100644 index 0000000000..e7e8eacc20 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/integration.cpp @@ -0,0 +1,56 @@ +#include "format.hpp" + +#include +#include +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + struct DetourNavigatorSerializationIntegrationTest : Test + { + Composite mComposite; + + DetourNavigatorSerializationIntegrationTest() + { + mComposite.mIntVector = {4, 5, 6}; + mComposite.mEnumVector = {Enum::A, Enum::B, Enum::C}; + mComposite.mPodVector = {Pod {4, 23.87}, Pod {5, -31.76}, Pod {6, 65.12}}; + mComposite.mPodBuffer = {Pod {7, 456.123}, Pod {8, -628.346}}; + mComposite.mPodDataSize = mComposite.mPodBuffer.size(); + std::string charData = "serialization"; + mComposite.mCharBuffer = {charData.begin(), charData.end()}; + mComposite.mCharDataSize = charData.size(); + } + }; + + TEST_F(DetourNavigatorSerializationIntegrationTest, sizeAccumulatorShouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + TestFormat{}(sizeAccumulator, mComposite); + EXPECT_EQ(sizeAccumulator.value(), 143); + } + + TEST_F(DetourNavigatorSerializationIntegrationTest, binaryReaderShouldDeserializeDataWrittenByBinaryWriter) + { + std::vector data(143); + TestFormat{}(BinaryWriter(data.data(), data.data() + data.size()), mComposite); + Composite result; + TestFormat{}(BinaryReader(data.data(), data.data() + data.size()), result); + EXPECT_EQ(result.mIntVector, mComposite.mIntVector); + EXPECT_EQ(result.mEnumVector, mComposite.mEnumVector); + EXPECT_EQ(result.mPodVector, mComposite.mPodVector); + EXPECT_EQ(result.mPodDataSize, mComposite.mPodDataSize); + EXPECT_EQ(result.mPodBuffer, mComposite.mPodBuffer); + EXPECT_EQ(result.mCharDataSize, mComposite.mCharDataSize); + EXPECT_EQ(result.mCharBuffer, mComposite.mCharBuffer); + } +} diff --git a/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp new file mode 100644 index 0000000000..39b7ea8646 --- /dev/null +++ b/apps/openmw_test_suite/detournavigator/serialization/sizeaccumulator.cpp @@ -0,0 +1,43 @@ +#include "format.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace DetourNavigator::Serialization; + using namespace DetourNavigator::SerializationTesting; + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticType) + { + SizeAccumulator sizeAccumulator; + constexpr std::monostate format; + sizeAccumulator(format, std::uint32_t()); + EXPECT_EQ(sizeAccumulator.value(), 4); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldProvideSizeForArithmeticTypeRange) + { + SizeAccumulator sizeAccumulator; + const std::uint64_t* const data = nullptr; + const std::size_t count = 3; + const std::monostate format; + sizeAccumulator(format, data, count); + EXPECT_EQ(sizeAccumulator.value(), 24); + } + + TEST(DetourNavigatorSerializationSizeAccumulatorTest, shouldSupportCustomSerializer) + { + SizeAccumulator sizeAccumulator; + const TestFormat format; + sizeAccumulator(format, Pod {}); + EXPECT_EQ(sizeAccumulator.value(), 12); + } +} diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp index 6209ec9c2a..3667e946e0 100644 --- a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp @@ -264,7 +264,7 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - EXPECT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + EXPECT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles) @@ -272,9 +272,9 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); - for (int x = -6; x < 6; ++x) - for (int y = -6; y < 6; ++y) + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr); } @@ -286,7 +286,7 @@ namespace ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); const osg::Vec2i cellPosition(0, 0); const int cellSize = std::numeric_limits::max(); - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) ASSERT_EQ(manager.getMesh(TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); @@ -303,10 +303,10 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); const auto result = manager.removeWater(cellPosition); ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->mSize, cellSize); + EXPECT_EQ(result->mCellSize, cellSize); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles) @@ -314,7 +314,7 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); ASSERT_TRUE(manager.removeWater(cellPosition)); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) @@ -329,7 +329,7 @@ namespace ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); ASSERT_TRUE(manager.removeWater(cellPosition)); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) @@ -344,10 +344,10 @@ namespace const btBoxShape boxShape(btVector3(20, 20, 100)); const CollisionShape shape(nullptr, boxShape); ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, 0.0f)); ASSERT_TRUE(manager.removeObject(ObjectId(&boxShape))); - for (int x = -6; x < 6; ++x) - for (int y = -6; y < 6; ++y) + for (int x = -1; x < 12; ++x) + for (int y = -1; y < 12; ++y) ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr); } } diff --git a/apps/openmw_test_suite/esmloader/esmdata.cpp b/apps/openmw_test_suite/esmloader/esmdata.cpp new file mode 100644 index 0000000000..da78aaa3a2 --- /dev/null +++ b/apps/openmw_test_suite/esmloader/esmdata.cpp @@ -0,0 +1,101 @@ +#include + +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct Params + { + std::string mRefId; + ESM::RecNameInts mType; + std::string mResult; + std::function mPushBack; + }; + + struct EsmLoaderGetModelTest : TestWithParam {}; + + TEST_P(EsmLoaderGetModelTest, shouldReturnFoundModelName) + { + EsmData data; + GetParam().mPushBack(data); + EXPECT_EQ(EsmLoader::getModel(data, GetParam().mRefId, GetParam().mType), GetParam().mResult); + } + + void pushBack(ESM::Activator&& value, EsmData& esmData) + { + esmData.mActivators.push_back(std::move(value)); + } + + void pushBack(ESM::Container&& value, EsmData& esmData) + { + esmData.mContainers.push_back(std::move(value)); + } + + void pushBack(ESM::Door&& value, EsmData& esmData) + { + esmData.mDoors.push_back(std::move(value)); + } + + void pushBack(ESM::Static&& value, EsmData& esmData) + { + esmData.mStatics.push_back(std::move(value)); + } + + template + struct PushBack + { + std::string mId; + std::string mModel; + + void operator()(EsmData& esmData) const + { + T value; + value.mId = mId; + value.mModel = mModel; + pushBack(std::move(value), esmData); + } + }; + + const std::array params = { + Params {"acti_ref_id", ESM::REC_ACTI, "acti_model", PushBack {"acti_ref_id", "acti_model"}}, + Params {"cont_ref_id", ESM::REC_CONT, "cont_model", PushBack {"cont_ref_id", "cont_model"}}, + Params {"door_ref_id", ESM::REC_DOOR, "door_model", PushBack {"door_ref_id", "door_model"}}, + Params {"static_ref_id", ESM::REC_STAT, "static_model", PushBack {"static_ref_id", "static_model"}}, + Params {"acti_ref_id_a", ESM::REC_ACTI, "", PushBack {"acti_ref_id_z", "acti_model"}}, + Params {"cont_ref_id_a", ESM::REC_CONT, "", PushBack {"cont_ref_id_z", "cont_model"}}, + Params {"door_ref_id_a", ESM::REC_DOOR, "", PushBack {"door_ref_id_z", "door_model"}}, + Params {"static_ref_id_a", ESM::REC_STAT, "", PushBack {"static_ref_id_z", "static_model"}}, + Params {"acti_ref_id_z", ESM::REC_ACTI, "", PushBack {"acti_ref_id_a", "acti_model"}}, + Params {"cont_ref_id_z", ESM::REC_CONT, "", PushBack {"cont_ref_id_a", "cont_model"}}, + Params {"door_ref_id_z", ESM::REC_DOOR, "", PushBack {"door_ref_id_a", "door_model"}}, + Params {"static_ref_id_z", ESM::REC_STAT, "", PushBack {"static_ref_id_a", "static_model"}}, + Params {"ref_id", ESM::REC_STAT, "", [] (EsmData&) {}}, + Params {"ref_id", ESM::REC_BOOK, "", [] (EsmData&) {}}, + }; + + INSTANTIATE_TEST_SUITE_P(Params, EsmLoaderGetModelTest, ValuesIn(params)); + + TEST(EsmLoaderGetGameSettingTest, shouldReturnFoundValue) + { + std::vector settings; + ESM::GameSetting setting; + setting.mId = "setting"; + setting.mValue = ESM::Variant(42); + settings.push_back(setting); + EXPECT_EQ(EsmLoader::getGameSetting(settings, "setting"), ESM::Variant(42)); + } + + TEST(EsmLoaderGetGameSettingTest, shouldThrowExceptionWhenNotFound) + { + const std::vector settings; + EXPECT_THROW(EsmLoader::getGameSetting(settings, "setting"), std::runtime_error); + } +} diff --git a/apps/openmw_test_suite/esmloader/load.cpp b/apps/openmw_test_suite/esmloader/load.cpp new file mode 100644 index 0000000000..d6ed66f7a7 --- /dev/null +++ b/apps/openmw_test_suite/esmloader/load.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include + +#include + +#ifndef OPENMW_DATA_DIR +#error "OPENMW_DATA_DIR is not defined" +#endif + +namespace +{ + using namespace testing; + using namespace EsmLoader; + + struct EsmLoaderTest : Test + { + const Files::PathContainer mDataDirs {{std::string(OPENMW_DATA_DIR)}}; + const Files::Collections mFileCollections {mDataDirs, true}; + const std::vector mContentFiles {{"template.omwgame"}}; + }; + + TEST_F(EsmLoaderTest, loadEsmDataShouldSupportOmwgame) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + std::vector readers(mContentFiles.size()); + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsWhenQueryLoadCellsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = false; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + std::vector readers(mContentFiles.size()); + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 1521); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreCellsGameSettingsWhenQueryLoadGameSettingsIsFalse) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = false; + query.mLoadLands = true; + query.mLoadStatics = true; + std::vector readers(mContentFiles.size()); + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 1); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 1); + EXPECT_EQ(esmData.mStatics.size(), 2); + } + + TEST_F(EsmLoaderTest, shouldIgnoreAllWithDefaultQuery) + { + const Query query; + std::vector readers(mContentFiles.size()); + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, mContentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } + + TEST_F(EsmLoaderTest, loadEsmDataShouldSkipUnsupportedFormats) + { + Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const std::vector contentFiles {{"script.omwscripts"}}; + std::vector readers(contentFiles.size()); + ToUTF8::Utf8Encoder* const encoder = nullptr; + const EsmData esmData = loadEsmData(query, contentFiles, mFileCollections, readers, encoder); + EXPECT_EQ(esmData.mActivators.size(), 0); + EXPECT_EQ(esmData.mCells.size(), 0); + EXPECT_EQ(esmData.mContainers.size(), 0); + EXPECT_EQ(esmData.mDoors.size(), 0); + EXPECT_EQ(esmData.mGameSettings.size(), 0); + EXPECT_EQ(esmData.mLands.size(), 0); + EXPECT_EQ(esmData.mStatics.size(), 0); + } +} diff --git a/apps/openmw_test_suite/files/hash.cpp b/apps/openmw_test_suite/files/hash.cpp new file mode 100644 index 0000000000..e6dbc8f6cc --- /dev/null +++ b/apps/openmw_test_suite/files/hash.cpp @@ -0,0 +1,55 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Files; + + struct Params + { + std::size_t mSize; + std::array mHash; + }; + + struct FilesGetHash : TestWithParam {}; + + TEST_P(FilesGetHash, shouldReturnHashForStringStream) + { + const std::string fileName = "fileName"; + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + std::istringstream stream(content); + EXPECT_EQ(getHash(fileName, stream), GetParam().mHash); + } + + TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream) + { + std::string fileName(UnitTest::GetInstance()->current_test_info()->name()); + std::replace(fileName.begin(), fileName.end(), '/', '_'); + std::string content; + std::fill_n(std::back_inserter(content), GetParam().mSize, 'a'); + std::fstream(fileName, std::ios_base::out | std::ios_base::binary) + .write(content.data(), static_cast(content.size())); + const auto stream = Files::openConstrainedFileStream(fileName.data(), 0, content.size()); + EXPECT_EQ(getHash(fileName, *stream), GetParam().mHash); + } + + INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash, Values( + Params {0, {0, 0}}, + Params {1, {9607679276477937801ull, 16624257681780017498ull}}, + Params {128, {15287858148353394424ull, 16818615825966581310ull}}, + Params {1000, {11018119256083894017ull, 6631144854802791578ull}}, + Params {4096, {11972283295181039100ull, 16027670129106775155ull}}, + Params {4097, {16717956291025443060ull, 12856404199748778153ull}}, + Params {5000, {15775925571142117787ull, 10322955217889622896ull}} + )); +} diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp new file mode 100644 index 0000000000..054ea8cbda --- /dev/null +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -0,0 +1,58 @@ +#include "gmock/gmock.h" +#include + +#include + +#include "testing_util.hpp" + +namespace +{ + + TEST(LuaConfigurationTest, ValidConfiguration) + { + ESM::LuaScriptsCfg cfg; + LuaUtil::parseOMWScripts(cfg, R"X( + # Lines starting with '#' are comments + GLOBAL: my_mod/#some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER :my_mod/player.lua + CUSTOM : my_mod/some_other_script.lua + NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X"); + LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n"); + + ASSERT_EQ(cfg.mScripts.size(), 6); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA"); + EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua"); + + LuaUtil::ScriptsConfiguration conf; + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 3); + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua"); + // cfg.mScripts[1] is overridden by cfg.mScripts[4] + // cfg.mScripts[2] is overridden by cfg.mScripts[3] + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua"); + // cfg.mScripts[4] is removed because there are no flags + EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua"); + + cfg = ESM::LuaScriptsCfg(); + conf.init(std::move(cfg)); + ASSERT_EQ(conf.size(), 0); + } + + TEST(LuaConfigurationTest, Errors) + { + ESM::LuaScriptsCfg cfg; + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"), + "Lua script should have suffix '.lua', got: GLOBAL: something"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"), + "No flags found in: something.lua"); + EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"), + "Global script can not have local flags"); + } + +} diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 32d4ea49b8..4b3ecdcb2b 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -58,7 +58,8 @@ return { {"invalid.lua", &invalidScriptFile} }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; }; TEST_F(LuaStateTest, Sandbox) @@ -148,7 +149,7 @@ return { TEST_F(LuaStateTest, ProvideAPI) { - LuaUtil::LuaState lua(mVFS.get()); + LuaUtil::LuaState lua(mVFS.get(), &mCfg); sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); diff --git a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp b/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp deleted file mode 100644 index b1526ef9b6..0000000000 --- a/apps/openmw_test_suite/lua/test_omwscriptsparser.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "gmock/gmock.h" -#include - -#include - -#include "testing_util.hpp" - -namespace -{ - using namespace testing; - - TestFile file1( - "#comment.lua\n" - "\n" - "script1.lua\n" - "some mod/Some Script.lua" - ); - TestFile file2( - "#comment.lua\r\n" - "\r\n" - "script2.lua\r\n" - "some other mod/Some Script.lua\r" - ); - TestFile emptyFile(""); - TestFile invalidFile("Invalid file"); - - struct OMWScriptsParserTest : Test - { - std::unique_ptr mVFS = createTestVFS({ - {"file1.omwscripts", &file1}, - {"file2.omwscripts", &file2}, - {"empty.omwscripts", &emptyFile}, - {"invalid.lua", &file1}, - {"invalid.omwscripts", &invalidFile}, - }); - }; - - TEST_F(OMWScriptsParserTest, Basic) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"file2.omwscripts", "empty.omwscripts", "file1.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), ""); - EXPECT_THAT(res, ElementsAre("script2.lua", "some other mod/Some Script.lua", - "script1.lua", "some mod/Some Script.lua")); - } - - TEST_F(OMWScriptsParserTest, InvalidFiles) - { - internal::CaptureStdout(); - std::vector res = LuaUtil::parseOMWScriptsFiles( - mVFS.get(), {"invalid.lua", "invalid.omwscripts"}); - EXPECT_EQ(internal::GetCapturedStdout(), - "Script list should have suffix '.omwscripts', got: 'invalid.lua'\n" - "Lua script should have suffix '.lua', got: 'Invalid file'\n"); - EXPECT_THAT(res, ElementsAre()); - } - -} diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 8f05138782..8be02a2f13 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -18,7 +18,10 @@ namespace TestFile testScript(R"X( return { - engineHandlers = { onUpdate = function(dt) print(' update ' .. tostring(dt)) end }, + engineHandlers = { + onUpdate = function(dt) print(' update ' .. tostring(dt)) end, + onLoad = function() print('load') end, + }, eventHandlers = { Event1 = function(eventData) print(' event1 ' .. tostring(eventData.x)) end, Event2 = function(eventData) print(' event2 ' .. tostring(eventData.x)) end, @@ -75,15 +78,25 @@ return { )X"); TestFile overrideInterfaceScript(R"X( -local old = require('openmw.interfaces').TestInterface +local old = nil +local interface = { + fn = function(x) + print('NEW FN', x) + old.fn(x) + end, + value, +} return { interfaceName = "TestInterface", - interface = { - fn = function(x) - print('NEW FN', x) - old.fn(x) - end, - value = old.value + 1 + interface = interface, + engineHandlers = { + onInit = function() print('init') end, + onLoad = function() print('load') end, + onInterfaceOverride = function(oldInterface) + print('override') + old = oldInterface + interface.value = oldInterface.value + 1 + end }, } )X"); @@ -115,7 +128,25 @@ return { {"useInterface.lua", &useInterfaceScript}, }); - LuaUtil::LuaState mLua{mVFS.get()}; + LuaUtil::ScriptsConfiguration mCfg; + LuaUtil::LuaState mLua{mVFS.get(), &mCfg}; + + LuaScriptsContainerTest() + { + ESM::LuaScriptsCfg cfg; + cfg.mScripts.push_back({"invalid.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"incorrect.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"empty.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test1.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"stopEvent.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"test2.lua", "", ESM::LuaScriptCfg::sCustom}); + cfg.mScripts.push_back({"loadSave1.lua", "", ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"loadSave2.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sNPC}); + cfg.mScripts.push_back({"testInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"overrideInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + cfg.mScripts.push_back({"useInterface.lua", "", ESM::LuaScriptCfg::sCustom | ESM::LuaScriptCfg::sPlayer}); + mCfg.init(std::move(cfg)); + } }; TEST_F(LuaScriptsContainerTest, VerifyStructure) @@ -123,21 +154,21 @@ return { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); { testing::internal::CaptureStdout(); - EXPECT_FALSE(scripts.addNewScript("invalid.lua")); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Can't start Test[invalid.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("incorrect.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("incorrect.lua"))); std::string output = testing::internal::GetCapturedStdout(); EXPECT_THAT(output, HasSubstr("Not supported handler 'incorrectHandler' in Test[incorrect.lua]")); EXPECT_THAT(output, HasSubstr("Not supported section 'incorrectSection' in Test[incorrect.lua]")); } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("empty.lua")); - EXPECT_FALSE(scripts.addNewScript("empty.lua")); // already present + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); + EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present EXPECT_EQ(internal::GetCapturedStdout(), ""); } } @@ -146,9 +177,9 @@ return { { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\t update 1.5\n" "Test[test2.lua]:\t update 1.5\n"); @@ -157,9 +188,9 @@ return { TEST_F(LuaScriptsContainerTest, CallEvent) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5)); @@ -204,9 +235,9 @@ return { TEST_F(LuaScriptsContainerTest, RemoveScript) { LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("stopEvent.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua"))); + EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua"))); std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5)); { @@ -221,8 +252,10 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("stopEvent.lua")); - EXPECT_FALSE(scripts.removeScript("stopEvent.lua")); // already removed + int stopEventScriptId = *mCfg.findId("stopEvent.lua"); + EXPECT_TRUE(scripts.hasScript(stopEventScriptId)); + scripts.removeScript(stopEventScriptId); + EXPECT_FALSE(scripts.hasScript(stopEventScriptId)); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -233,7 +266,7 @@ return { } { testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.removeScript("test1.lua")); + scripts.removeScript(*mCfg.findId("test1.lua")); scripts.update(1.5f); scripts.receiveEvent("Event1", X); EXPECT_EQ(internal::GetCapturedStdout(), @@ -242,17 +275,41 @@ return { } } + TEST_F(LuaScriptsContainerTest, AutoStart) + { + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); + testing::internal::CaptureStdout(); + scripts.addAutoStartedScripts(); + scripts.update(1.5f); + EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" + "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" + "Test[testInterface.lua]:\tFN\t4.5\n"); + } + TEST_F(LuaScriptsContainerTest, Interface) { - LuaUtil::ScriptsContainer scripts(&mLua, "Test"); + LuaUtil::ScriptsContainer scripts(&mLua, "Test", ESM::LuaScriptCfg::sCreature); + int addIfaceId = *mCfg.findId("testInterface.lua"); + int overrideIfaceId = *mCfg.findId("overrideInterface.lua"); + int useIfaceId = *mCfg.findId("useInterface.lua"); + testing::internal::CaptureStdout(); - EXPECT_TRUE(scripts.addNewScript("testInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("overrideInterface.lua")); - EXPECT_TRUE(scripts.addNewScript("useInterface.lua")); + scripts.addAutoStartedScripts(); scripts.update(1.5f); - EXPECT_TRUE(scripts.removeScript("overrideInterface.lua")); + EXPECT_EQ(internal::GetCapturedStdout(), ""); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(addIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(overrideIfaceId)); + EXPECT_TRUE(scripts.addCustomScript(useIfaceId)); + scripts.update(1.5f); + scripts.removeScript(overrideIfaceId); scripts.update(1.5f); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[overrideInterface.lua]:\tNEW FN\t4.5\n" "Test[testInterface.lua]:\tFN\t4.5\n" "Test[testInterface.lua]:\tFN\t3.5\n"); @@ -260,16 +317,12 @@ return { TEST_F(LuaScriptsContainerTest, LoadSave) { - LuaUtil::ScriptsContainer scripts1(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts2(&mLua, "Test"); - LuaUtil::ScriptsContainer scripts3(&mLua, "Test"); - - EXPECT_TRUE(scripts1.addNewScript("loadSave1.lua")); - EXPECT_TRUE(scripts1.addNewScript("test1.lua")); - EXPECT_TRUE(scripts1.addNewScript("loadSave2.lua")); + LuaUtil::ScriptsContainer scripts1(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts2(&mLua, "Test", ESM::LuaScriptCfg::sNPC); + LuaUtil::ScriptsContainer scripts3(&mLua, "Test", ESM::LuaScriptCfg::sPlayer); - EXPECT_TRUE(scripts3.addNewScript("test2.lua")); - EXPECT_TRUE(scripts3.addNewScript("loadSave2.lua")); + scripts1.addAutoStartedScripts(); + EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua"))); scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with( "n", 1, @@ -282,23 +335,30 @@ return { ESM::LuaScripts data; scripts1.save(data); - scripts2.load(data, true); - scripts3.load(data, false); { testing::internal::CaptureStdout(); + scripts2.load(data); scripts2.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Test[test1.lua]:\tload\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test1.lua]:\tprint\n" - "Test[loadSave1.lua]:\t2.5\t1.5\n"); + "Test[loadSave1.lua]:\t2.5\t1.5\n" + "Test[test1.lua]:\tprint\n"); + EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua"))); } { testing::internal::CaptureStdout(); + scripts3.load(data); scripts3.receiveEvent("Print", ""); EXPECT_EQ(internal::GetCapturedStdout(), + "Ignoring Test[loadSave1.lua]; this script is not allowed here\n" + "Test[test1.lua]:\tload\n" + "Test[overrideInterface.lua]:\toverride\n" + "Test[overrideInterface.lua]:\tinit\n" "Test[loadSave2.lua]:\t0.5\t3.5\n" - "Test[test2.lua]:\tprint\n"); + "Test[test1.lua]:\tprint\n"); + EXPECT_TRUE(scripts3.hasScript(*mCfg.findId("testInterface.lua"))); } } @@ -306,8 +366,13 @@ return { { using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); - EXPECT_TRUE(scripts.addNewScript("test1.lua")); - EXPECT_TRUE(scripts.addNewScript("test2.lua")); + int test1Id = *mCfg.findId("test1.lua"); + int test2Id = *mCfg.findId("test2.lua"); + + testing::internal::CaptureStdout(); + EXPECT_TRUE(scripts.addCustomScript(test1Id)); + EXPECT_TRUE(scripts.addCustomScript(test2Id)); + EXPECT_EQ(internal::GetCapturedStdout(), ""); int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0; sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; }); @@ -315,25 +380,25 @@ return { sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; }); sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; }); - scripts.registerTimerCallback("test1.lua", "A", fn3); - scripts.registerTimerCallback("test1.lua", "B", fn4); - scripts.registerTimerCallback("test2.lua", "B", fn3); - scripts.registerTimerCallback("test2.lua", "A", fn4); + scripts.registerTimerCallback(test1Id, "A", fn3); + scripts.registerTimerCallback(test1Id, "B", fn4); + scripts.registerTimerCallback(test2Id, "B", fn3); + scripts.registerTimerCallback(test2Id, "A", fn4); scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, "test1.lua", "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 10, "test2.lua", "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, "test1.lua", "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 5, "test2.lua", "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, "test1.lua", "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, "test2.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, "test1.lua", fn2); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, "test2.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, "test1.lua", fn1); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, "test2.lua", fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1); EXPECT_EQ(counter1, 0); EXPECT_EQ(counter3, 0); @@ -358,10 +423,12 @@ return { EXPECT_EQ(counter3, 5); EXPECT_EQ(counter4, 5); + testing::internal::CaptureStdout(); ESM::LuaScripts data; scripts.save(data); - scripts.load(data, true); - scripts.registerTimerCallback("test1.lua", "B", fn4); + scripts.load(data); + scripts.registerTimerCallback(test1Id, "B", fn4); + EXPECT_EQ(internal::GetCapturedStdout(), "Test[test1.lua]:\tload\nTest[test2.lua]:\tload\n"); testing::internal::CaptureStdout(); scripts.processTimers(20, 20); @@ -373,4 +440,24 @@ return { EXPECT_EQ(counter4, 25); } + TEST_F(LuaScriptsContainerTest, CallbackWrapper) + { + LuaUtil::Callback callback{mLua.sol()["print"], mLua.newTable()}; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptDebugNameKey] = "some_script.lua"; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptsContainer::ScriptId{nullptr, 0}; + + testing::internal::CaptureStdout(); + callback(1.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\n"); + + testing::internal::CaptureStdout(); + callback(1.5, 2.5); + EXPECT_EQ(internal::GetCapturedStdout(), "1.5\t2.5\n"); + + testing::internal::CaptureStdout(); + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = sol::nil; + callback(1.5, 2.5); + EXPECT_EQ(internal::GetCapturedStdout(), "Ignored callback to the removed script some_script.lua\n"); + } + } diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/openmw_test_suite/lua/test_ui_content.cpp new file mode 100644 index 0000000000..f478c618dc --- /dev/null +++ b/apps/openmw_test_suite/lua/test_ui_content.cpp @@ -0,0 +1,97 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + + sol::state state; + + sol::table makeTable() + { + return sol::table(state, sol::create); + } + + sol::table makeTable(std::string name) + { + auto result = makeTable(); + result["name"] = name; + return result; + } + + TEST(LuaUiContentTest, Create) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + EXPECT_EQ(content.size(), 3); + } + + TEST(LuaUiContentTest, CreateWithHole) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table[4] = makeTable(); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, WrongType) + { + auto table = makeTable(); + table.add(makeTable()); + table.add("a"); + table.add(makeTable()); + EXPECT_ANY_THROW(LuaUi::Content content(table)); + } + + TEST(LuaUiContentTest, NameAccess) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable("a")); + LuaUi::Content content(table); + EXPECT_NO_THROW(content.at("a")); + content.remove("a"); + content.assign(content.size(), makeTable("b")); + content.assign("b", makeTable()); + EXPECT_ANY_THROW(content.at("b")); + EXPECT_EQ(content.size(), 2); + content.assign(content.size(), makeTable("c")); + content.assign(content.size(), makeTable("c")); + content.remove("c"); + EXPECT_ANY_THROW(content.at("c")); + } + + TEST(LuaUiContentTest, IndexOf) + { + auto table = makeTable(); + table.add(makeTable()); + table.add(makeTable()); + table.add(makeTable()); + LuaUi::Content content(table); + auto child = makeTable(); + content.assign(2, child); + EXPECT_EQ(content.indexOf(child), 2); + EXPECT_EQ(content.indexOf(makeTable()), content.size()); + } + + TEST(LuaUiContentTest, BoundsChecks) + { + auto table = makeTable(); + LuaUi::Content content(table); + EXPECT_ANY_THROW(content.at(0)); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + content.assign(content.size(), makeTable()); + EXPECT_ANY_THROW(content.at(3)); + EXPECT_ANY_THROW(content.remove(3)); + EXPECT_NO_THROW(content.remove(1)); + EXPECT_NO_THROW(content.at(1)); + EXPECT_EQ(content.size(), 2); + } +} diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp index 934cbf761a..21d2a344d4 100644 --- a/apps/openmw_test_suite/lua/test_utilpackage.cpp +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -91,15 +91,15 @@ namespace lua.safe_script("moveAndScale = T.move(v(1, 2, 3)) * T.scale(0.5, 1, 0.5) * T.move(10, 20, 30)"); EXPECT_EQ(getAsString(lua, "moveAndScale * v(0, 0, 0)"), "(6, 22, 18)"); EXPECT_EQ(getAsString(lua, "moveAndScale * v(300, 200, 100)"), "(156, 222, 68)"); - EXPECT_EQ(getAsString(lua, "moveAndScale"), "TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) }"); + EXPECT_THAT(getAsString(lua, "moveAndScale"), AllOf(StartsWith("TransformM{ move(6, 22, 18) scale(0.5, 1, 0.5) "), EndsWith(" }"))); EXPECT_EQ(getAsString(lua, "T.identity"), "TransformM{ }"); - lua.safe_script("rx = T.rotateX(math.pi / 2)"); - lua.safe_script("ry = T.rotateY(math.pi / 2)"); - lua.safe_script("rz = T.rotateZ(math.pi / 2)"); + lua.safe_script("rx = T.rotateX(-math.pi / 2)"); + lua.safe_script("ry = T.rotateY(-math.pi / 2)"); + lua.safe_script("rz = T.rotateZ(-math.pi / 2)"); EXPECT_LT(get(lua, "(rx * v(1, 2, 3) - v(1, -3, 2)):length()"), 1e-6); EXPECT_LT(get(lua, "(ry * v(1, 2, 3) - v(3, 2, -1)):length()"), 1e-6); EXPECT_LT(get(lua, "(rz * v(1, 2, 3) - v(-2, 1, 3)):length()"), 1e-6); - lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(-math.pi / 4)"); + lua.safe_script("rot = T.rotate(math.pi / 2, v(-1, -1, 0)) * T.rotateZ(math.pi / 4)"); EXPECT_THAT(getAsString(lua, "rot"), HasSubstr("TransformQ")); EXPECT_LT(get(lua, "(rot * v(1, 0, 0) - v(0, 0, 1)):length()"), 1e-6); EXPECT_LT(get(lua, "(rot * rot:inverse() * v(1, 0, 0) - v(1, 0, 0)):length()"), 1e-6); diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp index 28c4d59930..2f6810350f 100644 --- a/apps/openmw_test_suite/lua/testing_util.hpp +++ b/apps/openmw_test_suite/lua/testing_util.hpp @@ -52,7 +52,7 @@ namespace } #define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \ - catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); } + catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); } } diff --git a/apps/openmw_test_suite/misc/compression.cpp b/apps/openmw_test_suite/misc/compression.cpp new file mode 100644 index 0000000000..e062599f4a --- /dev/null +++ b/apps/openmw_test_suite/misc/compression.cpp @@ -0,0 +1,31 @@ +#include + +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + TEST(MiscCompressionTest, compressShouldAddPrefixWithDataSize) + { + const std::vector data(1234); + const std::vector compressed = compress(data); + int size = 0; + std::memcpy(&size, compressed.data(), sizeof(size)); + EXPECT_EQ(size, data.size()); + } + + TEST(MiscCompressionTest, decompressIsInverseToCompress) + { + const std::vector data(1024); + const std::vector compressed = compress(data); + EXPECT_LT(compressed.size(), data.size()); + const std::vector decompressed = decompress(compressed); + EXPECT_EQ(decompressed, data); + } +} diff --git a/apps/openmw_test_suite/misc/progressreporter.cpp b/apps/openmw_test_suite/misc/progressreporter.cpp new file mode 100644 index 0000000000..cd5449acf7 --- /dev/null +++ b/apps/openmw_test_suite/misc/progressreporter.cpp @@ -0,0 +1,43 @@ +#include + +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Misc; + + struct ReportMock + { + MOCK_METHOD(void, call, (std::size_t, std::size_t), ()); + }; + + struct Report + { + StrictMock* mImpl; + + void operator()(std::size_t provided, std::size_t expected) + { + mImpl->call(provided, expected); + } + }; + + TEST(MiscProgressReporterTest, shouldCallReportWhenPassedInterval) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).WillOnce(Return()); + ProgressReporter reporter(std::chrono::steady_clock::duration(0), Report {&report}); + reporter(13, 42); + } + + TEST(MiscProgressReporterTest, shouldNotCallReportWhenIntervalIsNotPassed) + { + StrictMock report; + EXPECT_CALL(report, call(13, 42)).Times(0); + ProgressReporter reporter(std::chrono::seconds(1000), Report {&report}); + reporter(13, 42); + } +} diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp new file mode 100644 index 0000000000..ee062c6da8 --- /dev/null +++ b/apps/openmw_test_suite/misc/test_resourcehelpers.cpp @@ -0,0 +1,80 @@ +#include +#include "components/misc/resourcehelpers.hpp" +#include "../lua/testing_util.hpp" + +namespace +{ + using namespace Misc::ResourceHelpers; + TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected) + { + std::unique_ptr mVFS = createTestVFS({ + {"sound/bar.wav", nullptr} + }); + EXPECT_EQ(correctSoundPath("sound/bar.wav", mVFS.get()), "sound/bar.wav"); + } + + TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected) + { + std::unique_ptr mVFS = createTestVFS({ + {"sound/foo.mp3", nullptr} + }); + EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); + } + + TEST(CorrectSoundPath, corrected_path_does_not_check_existence_in_vfs) + { + std::unique_ptr mVFS = createTestVFS({ + }); + EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3"); + } + + TEST(CorrectSoundPath, correct_path_normalize_paths) + { + std::unique_ptr mVFS = createTestVFS({ + }); + EXPECT_EQ(correctSoundPath("sound\\foo.wav", mVFS.get()), "sound/foo.mp3"); + EXPECT_EQ(correctSoundPath("SOUND\\foo.WAV", mVFS.get()), "SOUND/foo.mp3"); + } + + namespace + { + std::string checkChangeExtensionToDds(std::string path) + { + changeExtensionToDds(path); + return path; + } + } + + TEST(ChangeExtensionToDds, original_extension_with_same_size_as_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.tga"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_greater_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.jpeg"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, original_extension_smaller_than_dds) + { + EXPECT_EQ(checkChangeExtensionToDds("texture/bar.xx"), "texture/bar.dds"); + } + + TEST(ChangeExtensionToDds, does_not_change_dds_extension) + { + std::string path = "texture/bar.dds"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, does_not_change_when_no_extension) + { + std::string path = "texture/bar"; + EXPECT_FALSE(changeExtensionToDds(path)); + } + + TEST(ChangeExtensionToDds, change_when_there_is_an_extension) + { + std::string path = "texture/bar.jpeg"; + EXPECT_TRUE(changeExtensionToDds(path)); + } +} diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/openmw_test_suite/misc/test_stringops.cpp index 173cfa4447..7d8e93dc28 100644 --- a/apps/openmw_test_suite/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/misc/test_stringops.cpp @@ -1,5 +1,6 @@ #include #include "components/misc/stringops.hpp" +#include "components/misc/algorithm.hpp" #include #include @@ -18,7 +19,7 @@ struct PartialBinarySearchTest : public ::testing::Test bool matches(const std::string& keyword) { - return Misc::StringUtils::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); + return Misc::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); } }; diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_test_suite/mwscript/test_scripts.cpp new file mode 100644 index 0000000000..c04860afdf --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_scripts.cpp @@ -0,0 +1,849 @@ +#include +#include + +#include "test_utils.hpp" + +namespace +{ + struct MWScriptTest : public ::testing::Test + { + MWScriptTest() : mErrorHandler(), mParser(mErrorHandler, mCompilerContext) {} + + std::optional compile(const std::string& scriptBody, bool shouldFail = false) + { + mParser.reset(); + mErrorHandler.reset(); + std::istringstream input(scriptBody); + Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); + scanner.scan(mParser); + if(mErrorHandler.isGood()) + { + std::vector code; + mParser.getCode(code); + return CompiledScript(code, mParser.getLocals()); + } + else if(!shouldFail) + logErrors(); + return {}; + } + + void logErrors() + { + for(const auto& [error, loc] : mErrorHandler.getErrors()) + { + std::cout << error; + if(loc.mLine) + std::cout << " at line" << loc.mLine << " column " << loc.mColumn << " (" << loc.mLiteral << ")"; + std::cout << "\n"; + } + } + + void registerExtensions() + { + Compiler::registerExtensions(mExtensions); + mCompilerContext.setExtensions(&mExtensions); + } + + void run(const CompiledScript& script, TestInterpreterContext& context) + { + mInterpreter.run(&script.mByteCode[0], static_cast(script.mByteCode.size()), context); + } + + void installOpcode(int code, Interpreter::Opcode0* opcode) + { + mInterpreter.installSegment5(code, opcode); + } + protected: + void SetUp() override + { + Interpreter::installOpcodes(mInterpreter); + } + + void TearDown() override {} + private: + TestErrorHandler mErrorHandler; + TestCompilerContext mCompilerContext; + Compiler::FileParser mParser; + Compiler::Extensions mExtensions; + Interpreter::Interpreter mInterpreter; + }; + + const std::string sScript1 = R"mwscript(Begin basic_logic +; Comment +short one +short two + +set one to two + +if ( one == two ) + set one to 1 +elseif ( two == 1 ) + set one to 2 +else + set one to 3 +endif + +while ( one < two ) + set one to ( one + 1 ) +endwhile + +End)mwscript"; + + const std::string sScript2 = R"mwscript(Begin addtopic + +AddTopic "OpenMW Unit Test" + +End)mwscript"; + + const std::string sScript3 = R"mwscript(Begin math + +short a +short b +short c +short d +short e + +set b to ( a + 1 ) +set c to ( a - 1 ) +set d to ( b * c ) +set e to ( d / a ) + +End)mwscript"; + +// https://forum.openmw.org/viewtopic.php?f=6&t=2262 + const std::string sScript4 = R"mwscript(Begin scripting_once_again + +player -> addSpell "fire_bite", 645 + +PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 + +End)mwscript"; + + const std::string sIssue587 = R"mwscript(Begin stalresetScript + +End stalreset Script)mwscript"; + + const std::string sIssue677 = R"mwscript(Begin _ase_dtree_dtree-owls + +End)mwscript"; + + const std::string sIssue685 = R"mwscript(Begin issue685 + +Choice: "Sicher. Hier, nehmt." 1 "Nein, ich denke nicht. Tut mir Leid." 2 +StartScript GetPCGold + +End)mwscript"; + + const std::string sIssue694 = R"mwscript(Begin issue694 + +float timer + +if ( timer < .1 ) +endif + +End)mwscript"; + + const std::string sIssue1062 = R"mwscript(Begin issue1026 + +short end + +End)mwscript"; + + const std::string sIssue1430 = R"mwscript(Begin issue1430 + +short var +If ( menumode == 1 ) + Player->AddItem "fur_boots", 1 + Player->Equip "iron battle axe", 1 + player->addspell "fire bite", 645 + player->additem "ring_keley", 1, +endif + +End)mwscript"; + + const std::string sIssue1593 = R"mwscript(Begin changeWater_-550_400 + +End)mwscript"; + + const std::string sIssue1730 = R"mwscript(Begin 4LOM_Corprusarium_Guards + +End)mwscript"; + + const std::string sIssue1767 = R"mwscript(Begin issue1767 + +player->GetPcRank "temple" + +End)mwscript"; + + const std::string sIssue2185 = R"mwscript(Begin issue2185 + +short a +short b +short eq +short gte +short lte +short ne + +set eq to 0 +if ( a == b ) + set eq to ( eq + 1 ) +endif +if ( a = = b ) + set eq to ( eq + 1 ) +endif + +set gte to 0 +if ( a >= b ) + set gte to ( gte + 1 ) +endif +if ( a > = b ) + set gte to ( gte + 1 ) +endif + +set lte to 0 +if ( a <= b ) + set lte to ( lte + 1 ) +endif +if ( a < = b ) + set lte to ( lte + 1 ) +endif + +set ne to 0 +if ( a != b ) + set ne to ( ne + 1 ) +endif +if ( a ! = b ) + set ne to ( ne + 1 ) +endif + +End)mwscript"; + + const std::string sIssue2206 = R"mwscript(Begin issue2206 + +Choice ."Sklavin kaufen." 1 "Lebt wohl." 2 +Choice Choice "Insister pour qu’il vous réponde." 6 "Le prier de vous accorder un peu de son temps." 6 " Le menacer de révéler qu'il prélève sa part sur les bénéfices de la mine d’ébonite." 7 + +End)mwscript"; + + const std::string sIssue2207 = R"mwscript(Begin issue2207 + +PositionCell -35 –473 -248 0 "Skaal-Dorf, Die Große Halle" + +End)mwscript"; + + const std::string sIssue2794 = R"mwscript(Begin issue2794 + +if ( player->"getlevel" == 1 ) + ; do something +endif + +End)mwscript"; + + const std::string sIssue2830 = R"mwscript(Begin issue2830 + +AddItem "if" 1 +AddItem "endif" 1 +GetItemCount "begin" + +End)mwscript"; + + const std::string sIssue2991 = R"mwscript(Begin issue2991 + +MessageBox "OnActivate" +messagebox "messagebox" +messagebox "if" +messagebox "tcl" + +End)mwscript"; + + const std::string sIssue3006 = R"mwscript(Begin issue3006 + +short a + +if ( a == 1 ) + set a to 2 +else set a to 3 +endif + +End)mwscript"; + + const std::string sIssue3725 = R"mwscript(Begin issue3725 + +onactivate + +if onactivate + ; do something +endif + +End)mwscript"; + + const std::string sIssue3744 = R"mwscript(Begin issue3744 + +short a +short b +short c + +set c to 0 + +if ( a => b ) + set c to ( c + 1 ) +endif +if ( a =< b ) + set c to ( c + 1 ) +endif +if ( a = b ) + set c to ( c + 1 ) +endif +if ( a == b ) + set c to ( c + 1 ) +endif + +End)mwscript"; + + const std::string sIssue3836 = R"mwscript(Begin issue3836 + +MessageBox " Membership Level: %.0f +Account Balance: %.0f +Your Gold: %.0f +Interest Rate: %.3f +Service Charge Rate: %.3f +Total Service Charges: %.0f +Total Interest Earned: %.0f " Membership BankAccount YourGold InterestRate ServiceRate TotalServiceCharges TotalInterestEarned + +End)mwscript"; + + const std::string sIssue3846 = R"mwscript(Begin issue3846 + +Addtopic -spells... +Addtopic -magicka... + +End)mwscript"; + + const std::string sIssue4061 = R"mwscript(Begin 01_Rz_neuvazhay-koryto2 + +End)mwscript"; + + const std::string sIssue4451 = R"mwscript(Begin, GlassDisplayScript + +;[Script body] + +End, GlassDisplayScript)mwscript"; + + const std::string sIssue4597 = R"mwscript(Begin issue4597 + +short a +short b +short c +short d + +set c to 0 +set d to 0 + +if ( a <> b ) + set c to ( c + 1 ) +endif +if ( a << b ) + set c to ( c + 1 ) +endif +if ( a < b ) + set c to ( c + 1 ) +endif + +if ( a >< b ) + set d to ( d + 1 ) +endif +if ( a >> b ) + set d to ( d + 1 ) +endif +if ( a > b ) + set d to ( d + 1 ) +endif + +End)mwscript"; + + const std::string sIssue4598 = R"mwscript(Begin issue4598 + +StartScript kal_S_Pub_Jejubãr_Faraminos + +End)mwscript"; + + const std::string sIssue4803 = R"mwscript( +-- ++-Begin issue4803 + +End)mwscript"; + + const std::string sIssue4867 = R"mwscript(Begin issue4867 + +float PcMagickaMult : The gameplay setting fPcBaseMagickaMult - 1.0000 + +End)mwscript"; + + const std::string sIssue4888 = R"mwscript(Begin issue4888 + +if (player->GameHour == 10) +set player->GameHour to 20 +endif + +End)mwscript"; + + const std::string sIssue5087 = R"mwscript(Begin Begin + +player->sethealth 0 +stopscript Begin + +End Begin)mwscript"; + + const std::string sIssue5097 = R"mwscript(Begin issue5097 + +setscale "0.3" + +End)mwscript"; + + const std::string sIssue5345 = R"mwscript(Begin issue5345 + +StartScript DN_MinionDrain_s" + +End)mwscript"; + + const std::string sIssue6066 = R"mwscript(Begin issue6066 +addtopic "return" + +End)mwscript"; + + const std::string sIssue6282 = R"mwscript(Begin 11AA_LauraScript7.5 + +End)mwscript"; + + const std::string sIssue6363 = R"mwscript(Begin issue6363 + +short 1 + +if ( "1" == 1 ) + PositionCell 0 1 2 3 4 5 "Morrowland" +endif + +set 1 to 42 + +End)mwscript"; + + const std::string sIssue6380 = R"mwscript(,Begin,issue6380, + +,short,a + +,set,a,to,,,,(a,+1) + +messagebox,"this is a %g",a + +,End,)mwscript"; + + TEST_F(MWScriptTest, mwscript_test_invalid) + { + EXPECT_THROW(compile("this is not a valid script", true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_compilation) + { + EXPECT_FALSE(!compile(sScript1)); + } + + TEST_F(MWScriptTest, mwscript_test_no_extensions) + { + EXPECT_THROW(compile(sScript2, true), Compiler::SourceException); + } + + TEST_F(MWScriptTest, mwscript_test_function) + { + registerExtensions(); + bool failed = true; + if(const auto script = compile(sScript2)) + { + class AddTopic : public Interpreter::Opcode0 + { + bool& mFailed; + public: + AddTopic(bool& failed) : mFailed(failed) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + mFailed = false; + EXPECT_EQ(topic, "OpenMW Unit Test"); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(failed)); + TestInterpreterContext context; + run(*script, context); + } + if(failed) + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_math) + { + if(const auto script = compile(sScript3)) + { + struct Algorithm + { + int a; + int b; + int c; + int d; + int e; + + void run(int input) + { + a = input; + b = a + 1; + c = a - 1; + d = b * c; + e = d / a; + } + + void test(const TestInterpreterContext& context) const + { + EXPECT_EQ(a, context.getLocalShort(0)); + EXPECT_EQ(b, context.getLocalShort(1)); + EXPECT_EQ(c, context.getLocalShort(2)); + EXPECT_EQ(d, context.getLocalShort(3)); + EXPECT_EQ(e, context.getLocalShort(4)); + } + } algorithm; + TestInterpreterContext context; + for(int i = 1; i < 1000; ++i) + { + context.setLocalShort(0, i); + run(*script, context); + algorithm.run(i); + algorithm.test(context); + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_forum_thread) + { + registerExtensions(); + EXPECT_FALSE(!compile(sScript4)); + } + + TEST_F(MWScriptTest, mwscript_test_587) + { + EXPECT_FALSE(!compile(sIssue587)); + } + + TEST_F(MWScriptTest, mwscript_test_677) + { + EXPECT_FALSE(!compile(sIssue677)); + } + + TEST_F(MWScriptTest, mwscript_test_685) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue685)); + } + + TEST_F(MWScriptTest, mwscript_test_694) + { + EXPECT_FALSE(!compile(sIssue694)); + } + + TEST_F(MWScriptTest, mwscript_test_1062) + { + if(const auto script = compile(sIssue1062)) + { + EXPECT_EQ(script->mLocals.getIndex("end"), 0); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_1430) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1430)); + } + + TEST_F(MWScriptTest, mwscript_test_1593) + { + EXPECT_FALSE(!compile(sIssue1593)); + } + + TEST_F(MWScriptTest, mwscript_test_1730) + { + EXPECT_FALSE(!compile(sIssue1730)); + } + + TEST_F(MWScriptTest, mwscript_test_1767) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue1767)); + } + + TEST_F(MWScriptTest, mwscript_test_2185) + { + if(const auto script = compile(sIssue2185)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(3), a >= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(4), a <= b ? 2 : 0); + EXPECT_EQ(context.getLocalShort(5), a != b ? 2 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_2206) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2206)); + } + + TEST_F(MWScriptTest, mwscript_test_2207) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2207)); + } + + TEST_F(MWScriptTest, mwscript_test_2794) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2794)); + } + + TEST_F(MWScriptTest, mwscript_test_2830) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2830)); + } + + TEST_F(MWScriptTest, mwscript_test_2991) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue2991)); + } + + TEST_F(MWScriptTest, mwscript_test_3006) + { + if(const auto script = compile(sIssue3006)) + { + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 0); + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_EQ(context.getLocalShort(0), 2); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3725) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3725)); + } + + TEST_F(MWScriptTest, mwscript_test_3744) + { + if(const auto script = compile(sIssue3744)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a == b ? 4 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_3836) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue3836)); + } + + TEST_F(MWScriptTest, mwscript_test_3846) + { + registerExtensions(); + if(const auto script = compile(sIssue3846)) + { + std::vector topics = { "-spells...", "-magicka..." }; + class AddTopic : public Interpreter::Opcode0 + { + std::vector& mTopics; + public: + AddTopic(std::vector& topics) : mTopics(topics) {} + + void execute(Interpreter::Runtime& runtime) + { + const auto topic = runtime.getStringLiteral(runtime[0].mInteger); + runtime.pop(); + EXPECT_EQ(topic, mTopics[0]); + mTopics.erase(mTopics.begin()); + } + }; + installOpcode(Compiler::Dialogue::opcodeAddTopic, new AddTopic(topics)); + TestInterpreterContext context; + run(*script, context); + EXPECT_TRUE(topics.empty()); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4061) + { + EXPECT_FALSE(!compile(sIssue4061)); + } + + TEST_F(MWScriptTest, mwscript_test_4451) + { + EXPECT_FALSE(!compile(sIssue4451)); + } + + TEST_F(MWScriptTest, mwscript_test_4597) + { + if(const auto script = compile(sIssue4597)) + { + TestInterpreterContext context; + for(int a = 0; a < 100; ++a) + { + for(int b = 0; b < 100; ++b) + { + context.setLocalShort(0, a); + context.setLocalShort(1, b); + run(*script, context); + EXPECT_EQ(context.getLocalShort(2), a < b ? 3 : 0); + EXPECT_EQ(context.getLocalShort(3), a > b ? 3 : 0); + } + } + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_4598) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue4598)); + } + + TEST_F(MWScriptTest, mwscript_test_4803) + { + EXPECT_FALSE(!compile(sIssue4803)); + } + + TEST_F(MWScriptTest, mwscript_test_4867) + { + EXPECT_FALSE(!compile(sIssue4867)); + } + + TEST_F(MWScriptTest, mwscript_test_4888) + { + EXPECT_FALSE(!compile(sIssue4888)); + } + + TEST_F(MWScriptTest, mwscript_test_5087) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5087)); + } + + TEST_F(MWScriptTest, mwscript_test_5097) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5097)); + } + + TEST_F(MWScriptTest, mwscript_test_5345) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue5345)); + } + + TEST_F(MWScriptTest, mwscript_test_6066) + { + registerExtensions(); + EXPECT_FALSE(!compile(sIssue6066)); + } + + TEST_F(MWScriptTest, mwscript_test_6282) + { + EXPECT_FALSE(!compile(sIssue6282)); + } + + TEST_F(MWScriptTest, mwscript_test_6363) + { + registerExtensions(); + if(const auto script = compile(sIssue6363)) + { + class PositionCell : public Interpreter::Opcode0 + { + bool& mRan; + public: + PositionCell(bool& ran) : mRan(ran) {} + + void execute(Interpreter::Runtime& runtime) + { + mRan = true; + } + }; + bool ran = false; + installOpcode(Compiler::Transformation::opcodePositionCell, new PositionCell(ran)); + TestInterpreterContext context; + context.setLocalShort(0, 0); + run(*script, context); + EXPECT_FALSE(ran); + ran = false; + context.setLocalShort(0, 1); + run(*script, context); + EXPECT_TRUE(ran); + } + else + { + FAIL(); + } + } + + TEST_F(MWScriptTest, mwscript_test_6380) + { + EXPECT_FALSE(!compile(sIssue6380)); + } +} \ No newline at end of file diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_test_suite/mwscript/test_utils.hpp new file mode 100644 index 0000000000..f29cb7bb89 --- /dev/null +++ b/apps/openmw_test_suite/mwscript/test_utils.hpp @@ -0,0 +1,243 @@ +#ifndef MWSCRIPT_TESTING_UTIL_H +#define MWSCRIPT_TESTING_UTIL_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace +{ + class TestCompilerContext : public Compiler::Context + { + public: + bool canDeclareLocals() const override { return true; } + char getGlobalType(const std::string& name) const override { return ' '; } + std::pair getMemberType(const std::string& name, const std::string& id) const override { return {' ', false}; } + bool isId(const std::string& name) const override { return Misc::StringUtils::ciEqual(name, "player"); } + }; + + class TestErrorHandler : public Compiler::ErrorHandler + { + std::vector> mErrors; + + void report(const std::string& message, const Compiler::TokenLoc& loc, Compiler::ErrorHandler::Type type) override + { + if(type == Compiler::ErrorHandler::ErrorMessage) + mErrors.emplace_back(message, loc); + } + + void report(const std::string& message, Compiler::ErrorHandler::Type type) override + { + report(message, {}, type); + } + + public: + void reset() override + { + Compiler::ErrorHandler::reset(); + mErrors.clear(); + } + + const std::vector>& getErrors() const { return mErrors; } + }; + + class LocalVariables + { + std::vector mShorts; + std::vector mLongs; + std::vector mFloats; + + template + T getLocal(std::size_t index, const std::vector& vector) const + { + if(index < vector.size()) + return vector[index]; + return {}; + } + + template + void setLocal(T value, std::size_t index, std::vector& vector) + { + if(index >= vector.size()) + vector.resize(index + 1); + vector[index] = value; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(std::size_t index) const { return getLocal(index, mShorts); }; + + int getLong(std::size_t index) const { return getLocal(index, mLongs); }; + + float getFloat(std::size_t index) const { return getLocal(index, mFloats); }; + + void setShort(std::size_t index, int value) { setLocal(value, index, mShorts); }; + + void setLong(std::size_t index, int value) { setLocal(value, index, mLongs); }; + + void setFloat(std::size_t index, float value) { setLocal(value, index, mFloats); }; + }; + + class GlobalVariables + { + std::map mShorts; + std::map mLongs; + std::map mFloats; + + template + T getGlobal(const std::string& name, const std::map& map) const + { + auto it = map.find(name); + if(it != map.end()) + return it->second; + return {}; + } + public: + void clear() + { + mShorts.clear(); + mLongs.clear(); + mFloats.clear(); + } + + int getShort(const std::string& name) const { return getGlobal(name, mShorts); }; + + int getLong(const std::string& name) const { return getGlobal(name, mLongs); }; + + float getFloat(const std::string& name) const { return getGlobal(name, mFloats); }; + + void setShort(const std::string& name, int value) { mShorts[name] = value; }; + + void setLong(const std::string& name, int value) { mLongs[name] = value; }; + + void setFloat(const std::string& name, float value) { mFloats[name] = value; }; + }; + + class TestInterpreterContext : public Interpreter::Context + { + LocalVariables mLocals; + std::map mMembers; + public: + std::string getTarget() const override { return {}; }; + + int getLocalShort(int index) const override { return mLocals.getShort(index); }; + + int getLocalLong(int index) const override { return mLocals.getLong(index); }; + + float getLocalFloat(int index) const override { return mLocals.getFloat(index); }; + + void setLocalShort(int index, int value) override { mLocals.setShort(index, value); }; + + void setLocalLong(int index, int value) override { mLocals.setLong(index, value); }; + + void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); }; + + void messageBox(const std::string& message, const std::vector& buttons) override {}; + + void report(const std::string& message) override {}; + + int getGlobalShort(const std::string& name) const override { return {}; }; + + int getGlobalLong(const std::string& name) const override { return {}; }; + + float getGlobalFloat(const std::string& name) const override { return {}; }; + + void setGlobalShort(const std::string& name, int value) override {}; + + void setGlobalLong(const std::string& name, int value) override {}; + + void setGlobalFloat(const std::string& name, float value) override {}; + + std::vector getGlobals() const override { return {}; }; + + char getGlobalType(const std::string& name) const override { return ' '; }; + + std::string getActionBinding(const std::string& action) const override { return {}; }; + + std::string getActorName() const override { return {}; }; + + std::string getNPCRace() const override { return {}; }; + + std::string getNPCClass() const override { return {}; }; + + std::string getNPCFaction() const override { return {}; }; + + std::string getNPCRank() const override { return {}; }; + + std::string getPCName() const override { return {}; }; + + std::string getPCRace() const override { return {}; }; + + std::string getPCClass() const override { return {}; }; + + std::string getPCRank() const override { return {}; }; + + std::string getPCNextRank() const override { return {}; }; + + int getPCBounty() const override { return {}; }; + + std::string getCurrentCellName() const override { return {}; }; + + int getMemberShort(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getShort(name); + return {}; + }; + + int getMemberLong(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getLong(name); + return {}; + }; + + float getMemberFloat(const std::string& id, const std::string& name, bool global) const override + { + auto it = mMembers.find(id); + if(it != mMembers.end()) + return it->second.getFloat(name); + return {}; + }; + + void setMemberShort(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setShort(name, value); }; + + void setMemberLong(const std::string& id, const std::string& name, int value, bool global) override { mMembers[id].setLong(name, value); }; + + void setMemberFloat(const std::string& id, const std::string& name, float value, bool global) override { mMembers[id].setFloat(name, value); }; + }; + + struct CompiledScript + { + std::vector mByteCode; + Compiler::Locals mLocals; + + CompiledScript(const std::vector& code, const Compiler::Locals& locals) : mByteCode(code), mLocals(locals) {} + }; +} + +#endif \ No newline at end of file diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_test_suite/mwworld/test_store.cpp index 71b1fb0ff0..7ddab538c4 100644 --- a/apps/openmw_test_suite/mwworld/test_store.cpp +++ b/apps/openmw_test_suite/mwworld/test_store.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwmechanics/spelllist.hpp" @@ -27,19 +28,14 @@ struct ContentFileTest : public ::testing::Test readContentFiles(); // load the content files - std::vector readerList; - readerList.resize(mContentFiles.size()); - int index=0; for (const auto & mContentFile : mContentFiles) { ESM::ESMReader lEsm; lEsm.setEncoder(nullptr); lEsm.setIndex(index); - lEsm.setGlobalReaderList(&readerList); lEsm.open(mContentFile.string()); - readerList[index] = lEsm; - mEsmStore.load(readerList[index], &dummyListener); + mEsmStore.load(lEsm, &dummyListener); ++index; } @@ -87,7 +83,10 @@ struct ContentFileTest : public ::testing::Test std::vector contentFiles = variables["content"].as>(); for (auto & contentFile : contentFiles) - mContentFiles.push_back(collections.getPath(contentFile)); + { + if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts")) + mContentFiles.push_back(collections.getPath(contentFile)); + } } protected: @@ -249,9 +248,6 @@ TEST_F(StoreTest, delete_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); @@ -292,9 +288,6 @@ TEST_F(StoreTest, overwrite_test) record.mId = recordId; ESM::ESMReader reader; - std::vector readerList; - readerList.push_back(reader); - reader.setGlobalReaderList(&readerList); // master file inserts a record Files::IStreamPtr file = getEsmFile(record, false); diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp index a1f5d6091d..8fbf5c1b5b 100644 --- a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp +++ b/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp @@ -60,6 +60,19 @@ namespace { return isNear(lhs.getOrigin(), rhs.getOrigin()) && isNear(lhs.getBasis(), rhs.getBasis()); } + + struct WriteVec3f + { + osg::Vec3f mValue; + + friend std::ostream& operator <<(std::ostream& stream, const WriteVec3f& value) + { + return stream << "osg::Vec3f {" + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.x() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.y() << ", " + << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue.z() << "}"; + } + }; } static std::ostream& operator <<(std::ostream& stream, const btVector3& value) @@ -122,6 +135,17 @@ static std::ostream& operator <<(std::ostream& stream, const TriangleMeshShape& return stream << "}}"; } +static bool operator ==(const BulletShape::CollisionBox& l, const BulletShape::CollisionBox& r) +{ + const auto tie = [] (const BulletShape::CollisionBox& v) { return std::tie(v.mExtents, v.mCenter); }; + return tie(l) == tie(r); +} + +static std::ostream& operator <<(std::ostream& stream, const BulletShape::CollisionBox& value) +{ + return stream << "CollisionBox {" << WriteVec3f {value.mExtents} << ", " << WriteVec3f {value.mCenter} << "}"; +} + } static std::ostream& operator <<(std::ostream& stream, const btCollisionShape& value) @@ -160,20 +184,18 @@ namespace Resource { static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs) { - return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape) - && compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape) - && lhs.mCollisionBox.extents == rhs.mCollisionBox.extents - && lhs.mCollisionBox.center == rhs.mCollisionBox.center + return compareObjects(lhs.mCollisionShape.get(), rhs.mCollisionShape.get()) + && compareObjects(lhs.mAvoidCollisionShape.get(), rhs.mAvoidCollisionShape.get()) + && lhs.mCollisionBox == rhs.mCollisionBox && lhs.mAnimatedShapes == rhs.mAnimatedShapes; } static std::ostream& operator <<(std::ostream& stream, const Resource::BulletShape& value) { return stream << "Resource::BulletShape {" - << value.mCollisionShape << ", " - << value.mAvoidCollisionShape << ", " - << "osg::Vec3f {" << value.mCollisionBox.extents << "}" << ", " - << "osg::Vec3f {" << value.mCollisionBox.center << "}" << ", " + << value.mCollisionShape.get() << ", " + << value.mAvoidCollisionShape.get() << ", " + << value.mCollisionBox << ", " << value.mAnimatedShapes << "}"; } @@ -272,6 +294,12 @@ namespace value.recType = Nif::RC_NiTriShape; } + void init(Nif::NiTriStrips& value) + { + init(static_cast(value)); + value.recType = Nif::RC_NiTriStrips; + } + void init(Nif::NiSkinInstance& value) { value.data = Nif::NiSkinDataPtr(nullptr); @@ -307,6 +335,7 @@ namespace MOCK_METHOD(void, setUseSkinning, (bool), (override)); MOCK_METHOD(bool, getUseSkinning, (), (const, override)); MOCK_METHOD(std::string, getFilename, (), (const, override)); + MOCK_METHOD(std::string, getHash, (), (const, override)); MOCK_METHOD(unsigned int, getVersion, (), (const, override)); MOCK_METHOD(unsigned int, getUserVersion, (), (const, override)); MOCK_METHOD(unsigned int, getBethVersion, (), (const, override)); @@ -330,6 +359,8 @@ namespace Nif::NiTriShape mNiTriShape; Nif::NiTriShapeData mNiTriShapeData2; Nif::NiTriShape mNiTriShape2; + Nif::NiTriStripsData mNiTriStripsData; + Nif::NiTriStrips mNiTriStrips; Nif::NiSkinInstance mNiSkinInstance; Nif::NiStringExtraData mNiStringExtraData; Nif::NiStringExtraData mNiStringExtraData2; @@ -351,6 +382,7 @@ namespace ), btVector3(4, 8, 12) }; + const std::string mHash = "hash"; TestBulletNifLoader() { @@ -361,6 +393,7 @@ namespace init(mNiNode3); init(mNiTriShape); init(mNiTriShape2); + init(mNiTriStrips); init(mNiSkinInstance); init(mNiStringExtraData); init(mNiStringExtraData2); @@ -375,6 +408,13 @@ namespace mNiTriShapeData2.vertices = {osg::Vec3f(0, 0, 1), osg::Vec3f(1, 0, 1), osg::Vec3f(1, 1, 1)}; mNiTriShapeData2.triangles = {0, 1, 2}; mNiTriShape2.data = Nif::NiGeometryDataPtr(&mNiTriShapeData2); + + mNiTriStripsData.recType = Nif::RC_NiTriStripsData; + mNiTriStripsData.vertices = {osg::Vec3f(0, 0, 0), osg::Vec3f(1, 0, 0), osg::Vec3f(1, 1, 0), osg::Vec3f(0, 1, 0)}; + mNiTriStripsData.strips = {{0, 1, 2, 3}}; + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, getHash()).WillOnce(Return(mHash)); } }; @@ -386,6 +426,20 @@ namespace Resource::BulletShape expected; + EXPECT_EQ(*result, expected); + EXPECT_EQ(result->mFileName, "test.nif"); + EXPECT_EQ(result->mFileHash, mHash); + } + + TEST_F(TestBulletNifLoader, should_ignore_nullptr_root) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(nullptr)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + Resource::BulletShape expected; + EXPECT_EQ(*result, expected); } @@ -441,12 +495,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -458,6 +512,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNode)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -466,12 +521,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -483,6 +538,7 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -496,12 +552,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -513,11 +569,13 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -531,12 +589,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); std::unique_ptr box(new btBoxShape(btVector3(1, 2, 3))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -547,12 +605,14 @@ namespace mNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode.bounds.box.extents = osg::Vec3f(1, 2, 3); mNode.bounds.box.center = osg::Vec3f(-1, -2, -3); + mNode.parent = &mNiNode; mNode2.hasBounds = true; mNode2.flags |= Nif::NiNode::Flag_BBoxCollision; mNode2.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; mNode2.bounds.box.extents = osg::Vec3f(4, 5, 6); mNode2.bounds.box.center = osg::Vec3f(-4, -5, -6); + mNode2.parent = &mNiNode; mNiNode.hasBounds = true; mNiNode.bounds.type = Nif::NiBoundingVolume::Type::BOX_BV; @@ -566,12 +626,12 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(4, 5, 6); - expected.mCollisionBox.center = osg::Vec3f(-4, -5, -6); + expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6); + expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6); std::unique_ptr box(new btBoxShape(btVector3(4, 5, 6))); std::unique_ptr shape(new btCompoundShape); shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release()); - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); EXPECT_EQ(*result, expected); } @@ -589,8 +649,8 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } @@ -605,7 +665,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -623,14 +683,15 @@ namespace const auto result = mLoader.load(mNifFile); Resource::BulletShape expected; - expected.mCollisionBox.extents = osg::Vec3f(1, 2, 3); - expected.mCollisionBox.center = osg::Vec3f(-1, -2, -3); + expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3); + expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_tri_shape_child_node_should_return_shape_with_triangle_mesh_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -641,7 +702,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -649,7 +710,9 @@ namespace TEST_F(TestBulletNifLoader, for_nested_tri_shape_child_should_return_shape_with_triangle_mesh_shape) { mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); + mNiNode2.parent = &mNiNode; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiTriShape.parent = &mNiNode2; EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); @@ -659,13 +722,15 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } TEST_F(TestBulletNifLoader, for_two_tri_shape_children_should_return_shape_with_triangle_mesh_shape_with_all_meshes) { + mNiTriShape.parent = &mNiNode; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), Nif::NodePtr(&mNiTriShape2) @@ -680,7 +745,7 @@ namespace triangles->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -688,6 +753,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_and_filename_starting_with_x_and_not_empty_skin_should_return_shape_with_triangle_mesh_shape) { mNiTriShape.skin = Nif::NiSkinInstancePtr(&mNiSkinInstance); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -698,7 +764,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); EXPECT_EQ(*result, expected); } @@ -720,7 +786,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -746,7 +812,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -756,9 +822,11 @@ namespace { copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; + mNiTriShape2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({ Nif::NodePtr(&mNiTriShape), @@ -784,7 +852,7 @@ namespace shape->addChildShape(mResultTransform, mesh.release()); shape->addChildShape(mResultTransform, mesh2.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -813,7 +881,7 @@ namespace std::unique_ptr shape(new btCompoundShape); shape->addChildShape(mResultTransform2, mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -825,6 +893,7 @@ namespace mController.flags |= Nif::NiNode::ControllerFlag_Active; copy(mTransform, mNiTriShape.trafo); mNiTriShape.trafo.scale = 3; + mNiTriShape.parent = &mNiNode; copy(mTransform, mNiTriShape2.trafo); mNiTriShape2.trafo.scale = 3; mNiTriShape2.parent = &mNiNode; @@ -841,7 +910,7 @@ namespace const auto result = mLoader.load(mNifFile); std::unique_ptr triangles(new btTriangleMesh(false)); - triangles->addTriangle(btVector3(1, 2, 3), btVector3(4, 2, 3), btVector3(4, 4.632747650146484375, 1.56172335147857666015625)); + triangles->addTriangle(btVector3(4, 8, 12), btVector3(16, 8, 12), btVector3(16, 18.5309906005859375, 6.246893405914306640625)); std::unique_ptr mesh(new Resource::TriangleMeshShape(triangles.release(), true)); mesh->setLocalScaling(btVector3(1, 1, 1)); @@ -854,7 +923,35 @@ namespace shape->addChildShape(mResultTransform2, mesh2.release()); shape->addChildShape(btTransform::getIdentity(), mesh.release()); Resource::BulletShape expected; - expected.mCollisionShape = shape.release(); + expected.mCollisionShape.reset(shape.release()); + expected.mAnimatedShapes = {{-1, 0}}; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_add_static_mesh_to_existing_compound_mesh) + { + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriShape2)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr triangles2(new btTriangleMesh(false)); + triangles2->addTriangle(btVector3(0, 0, 1), btVector3(1, 0, 1), btVector3(1, 1, 1)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles2.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); @@ -862,6 +959,7 @@ namespace TEST_F(TestBulletNifLoader, for_root_avoid_node_and_tri_shape_child_node_should_return_shape_with_null_collision_shape) { + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode.recType = Nif::RC_AvoidNode; @@ -873,7 +971,7 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false); + expected.mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), false)); EXPECT_EQ(*result, expected); } @@ -881,6 +979,7 @@ namespace TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_empty_data_should_return_shape_with_null_collision_shape) { mNiTriShape.data = Nif::NiGeometryDataPtr(nullptr); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -897,6 +996,7 @@ namespace { auto data = static_cast(mNiTriShape.data.getPtr()); data->triangles.clear(); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -914,6 +1014,7 @@ namespace mNiStringExtraData.string = "NC___"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -932,6 +1033,7 @@ namespace mNiStringExtraData2.string = "NC___"; mNiStringExtraData2.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -949,6 +1051,7 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); @@ -966,8 +1069,10 @@ namespace mNiStringExtraData.string = "MRK"; mNiStringExtraData.recType = Nif::RC_NiStringExtraData; mNiTriShape.extra = Nif::ExtraPtr(&mNiStringExtraData); + mNiTriShape.parent = &mNiNode2; mNiNode2.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); mNiNode2.recType = Nif::RC_RootCollisionNode; + mNiNode2.parent = &mNiNode; mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiNode2)})); mNiNode.recType = Nif::RC_NiNode; @@ -979,7 +1084,137 @@ namespace std::unique_ptr triangles(new btTriangleMesh(false)); triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); Resource::BulletShape expected; - expected.mCollisionShape = new Resource::TriangleMeshShape(triangles.release(), true); + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_shape_data_with_mismatching_data_rec_type) + { + mNiTriShape.data = Nif::NiGeometryDataPtr(&mNiTriStripsData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriShape)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_tri_strips_root_node_should_return_shape_with_triangle_mesh_shape) + { + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + triangles->addTriangle(btVector3(1, 0, 0), btVector3(0, 1, 0), btVector3(1, 1, 0)); + Resource::BulletShape expected; + expected.mCollisionShape.reset(new Resource::TriangleMeshShape(triangles.release(), true)); + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_mismatching_data_rec_type) + { + mNiTriStrips.data = Nif::NiGeometryDataPtr(&mNiTriShapeData); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_ignore_tri_strips_data_with_empty_strips) + { + mNiTriStripsData.strips.clear(); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_static_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_avoid_collision_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + mNiNode.recType = Nif::RC_AvoidNode; + mNiTriStripsData.strips.front() = {0, 1}; + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, for_animated_mesh_should_ignore_tri_strips_data_with_less_than_3_strips) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriStrips.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriStrips)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(1)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + const Resource::BulletShape expected; + + EXPECT_EQ(*result, expected); + } + + TEST_F(TestBulletNifLoader, should_not_add_static_mesh_with_no_triangles_to_compound_shape) + { + mNiTriStripsData.strips.front() = {0, 1}; + mNiTriShape.parent = &mNiNode; + mNiNode.children = Nif::NodeList(std::vector({Nif::NodePtr(&mNiTriShape)})); + + EXPECT_CALL(mNifFile, numRoots()).WillOnce(Return(2)); + EXPECT_CALL(mNifFile, getRoot(0)).WillOnce(Return(&mNiNode)); + EXPECT_CALL(mNifFile, getRoot(1)).WillOnce(Return(&mNiTriStrips)); + EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("xtest.nif")); + const auto result = mLoader.load(mNifFile); + + std::unique_ptr triangles(new btTriangleMesh(false)); + triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0)); + + std::unique_ptr compound(new btCompoundShape); + compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true)); + + Resource::BulletShape expected; + expected.mCollisionShape.reset(compound.release()); + expected.mAnimatedShapes = {{-1, 0}}; EXPECT_EQ(*result, expected); } diff --git a/apps/openmw_test_suite/sqlite3/db.cpp b/apps/openmw_test_suite/sqlite3/db.cpp new file mode 100644 index 0000000000..d2c0cd8674 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/db.cpp @@ -0,0 +1,15 @@ +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + TEST(Sqlite3DbTest, makeDbShouldCreateInMemoryDbWithSchema) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + EXPECT_NE(db, nullptr); + } +} diff --git a/apps/openmw_test_suite/sqlite3/request.cpp b/apps/openmw_test_suite/sqlite3/request.cpp new file mode 100644 index 0000000000..e0094827d1 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/request.cpp @@ -0,0 +1,270 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + template + struct InsertInt + { + static std::string_view text() noexcept { return "INSERT INTO ints (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, T value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertReal + { + static std::string_view text() noexcept { return "INSERT INTO reals (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, double value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertText + { + static std::string_view text() noexcept { return "INSERT INTO texts (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::string_view value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct InsertBlob + { + static std::string_view text() noexcept { return "INSERT INTO blobs (value) VALUES (:value)"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const std::vector& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetAll + { + std::string mQuery; + + explicit GetAll(const std::string& table) : mQuery("SELECT value FROM " + table + " ORDER BY value") {} + + std::string_view text() noexcept { return mQuery; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + template + struct GetExact + { + std::string mQuery; + + explicit GetExact(const std::string& table) : mQuery("SELECT value FROM " + table + " WHERE value = :value") {} + + std::string_view text() noexcept { return mQuery; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, const T& value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetInt64 + { + static std::string_view text() noexcept { return "SELECT value FROM ints WHERE value = :value"; } + + static void bind(sqlite3& db, sqlite3_stmt& statement, std::int64_t value) + { + bindParameter(db, statement, ":value", value); + } + }; + + struct GetNull + { + static std::string_view text() noexcept { return "SELECT NULL"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Int + { + int mValue = 0; + + Int() = default; + + explicit Int(int value) : mValue(value) {} + + Int& operator=(int value) + { + mValue = value; + return *this; + } + + friend bool operator==(const Int& l, const Int& r) + { + return l.mValue == r.mValue; + } + }; + + constexpr const char schema[] = R"( + CREATE TABLE ints ( value INTEGER ); + CREATE TABLE reals ( value REAL ); + CREATE TABLE texts ( value TEXT ); + CREATE TABLE blobs ( value BLOB ); + )"; + + struct Sqlite3RequestTest : Test + { + const Db mDb = makeDb(":memory:", schema); + }; + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(13), std::tuple(42))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + EXPECT_EQ(execute(*mDb, insert, 3.14), 1); + Statement select(*mDb, GetAll("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(3.14))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetAll("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, executeShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetAll("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportInt64) + { + Statement insert(*mDb, InsertInt {}); + const std::int64_t value = 1099511627776; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportReal) + { + Statement insert(*mDb, InsertReal {}); + const double value = 3.14; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("reals")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(value))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportText) + { + Statement insert(*mDb, InsertText {}); + const std::string text = "foo"; + EXPECT_EQ(execute(*mDb, insert, text), 1); + Statement select(*mDb, GetExact("texts")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), text); + EXPECT_THAT(result, ElementsAre(std::tuple(text))); + } + + TEST_F(Sqlite3RequestTest, requestShouldSupportBlob) + { + Statement insert(*mDb, InsertBlob {}); + const std::vector blob({std::byte(42), std::byte(13)}); + EXPECT_EQ(execute(*mDb, insert, blob), 1); + Statement select(*mDb, GetExact>("blobs")); + std::vector>> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), blob); + EXPECT_THAT(result, ElementsAre(std::tuple(blob))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportNull) + { + Statement select(*mDb, GetNull {}); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max()); + EXPECT_THAT(result, ElementsAre(std::tuple(nullptr))); + } + + TEST_F(Sqlite3RequestTest, requestResultShouldSupportConstructibleFromInt) + { + Statement insert(*mDb, InsertInt {}); + const int value = 42; + EXPECT_EQ(execute(*mDb, insert, value), 1); + Statement select(*mDb, GetExact("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), std::numeric_limits::max(), value); + EXPECT_THAT(result, ElementsAre(std::tuple(Int(value)))); + } + + TEST_F(Sqlite3RequestTest, requestShouldLimitOutput) + { + Statement insert(*mDb, InsertInt {}); + EXPECT_EQ(execute(*mDb, insert, 13), 1); + EXPECT_EQ(execute(*mDb, insert, 42), 1); + Statement select(*mDb, GetAll("ints")); + std::vector> result; + request(*mDb, select, std::back_inserter(result), 1); + EXPECT_THAT(result, ElementsAre(std::tuple(13))); + } +} diff --git a/apps/openmw_test_suite/sqlite3/statement.cpp b/apps/openmw_test_suite/sqlite3/statement.cpp new file mode 100644 index 0000000000..fb53ceebb2 --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/statement.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct Query + { + static std::string_view text() noexcept { return "SELECT 1"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + TEST(Sqlite3StatementTest, makeStatementShouldCreateStatementWithPreparedQuery) + { + const auto db = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + const Statement statement(*db, Query {}); + EXPECT_FALSE(statement.mNeedReset); + EXPECT_NE(statement.mHandle, nullptr); + EXPECT_EQ(statement.mQuery.text(), "SELECT 1"); + } +} diff --git a/apps/openmw_test_suite/sqlite3/transaction.cpp b/apps/openmw_test_suite/sqlite3/transaction.cpp new file mode 100644 index 0000000000..913fd34bce --- /dev/null +++ b/apps/openmw_test_suite/sqlite3/transaction.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace +{ + using namespace testing; + using namespace Sqlite3; + + struct InsertId + { + static std::string_view text() noexcept { return "INSERT INTO test (id) VALUES (42)"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct GetIds + { + static std::string_view text() noexcept { return "SELECT id FROM test"; } + static void bind(sqlite3&, sqlite3_stmt&) {} + }; + + struct Sqlite3TransactionTest : Test + { + const Db mDb = makeDb(":memory:", "CREATE TABLE test ( id INTEGER )"); + + void insertId() const + { + Statement insertId(*mDb, InsertId {}); + EXPECT_EQ(execute(*mDb, insertId), 1); + } + + std::vector> getIds() const + { + Statement getIds(*mDb, GetIds {}); + std::vector> result; + request(*mDb, getIds, std::back_inserter(result), std::numeric_limits::max()); + return result; + } + }; + + TEST_F(Sqlite3TransactionTest, shouldRollbackOnDestruction) + { + { + const Transaction transaction(*mDb); + insertId(); + } + EXPECT_THAT(getIds(), IsEmpty()); + } + + TEST_F(Sqlite3TransactionTest, commitShouldCommitTransaction) + { + { + Transaction transaction(*mDb); + insertId(); + transaction.commit(); + } + EXPECT_THAT(getIds(), ElementsAre(std::tuple(42))); + } +} diff --git a/cmake/FindGMock.cmake b/cmake/FindGMock.cmake index 502910a9df..bc3410969f 100644 --- a/cmake/FindGMock.cmake +++ b/cmake/FindGMock.cmake @@ -164,8 +164,16 @@ find_dependency(Threads) set_target_properties(GMock::GMock PROPERTIES INTERFACE_LINK_LIBRARIES "Threads::Threads" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GMOCK_LIBRARY}") + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX") + +if(EXISTS "${GMOCK_LIBRARY}") + set_target_properties(GMock::GMock PROPERTIES + IMPORTED_LOCATION "${GMOCK_LIBRARY}") +endif() +if(EXISTS "${GMOCK_LIBRARY_DEBUG}") + set_target_properties(GMock::GMock PROPERTIES + IMPORTED_LOCATION_DEBUG "${GMOCK_LIBRARY_DEBUG}") +endif() if(GMOCK_INCLUDE_DIR) set_target_properties(GMock::GMock PROPERTIES @@ -182,8 +190,16 @@ endif() set_target_properties(GMock::Main PROPERTIES INTERFACE_LINK_LIBRARIES "GMock::GMock" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX") + +if(EXISTS "${GMOCK_MAIN_LIBRARY}") + set_target_properties(GMock::Main PROPERTIES + IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY}") +endif() +if(EXISTS "${GMOCK_MAIN_LIBRARY_DEBUG}") + set_target_properties(GMock::Main PROPERTIES + IMPORTED_LOCATION "${GMOCK_MAIN_LIBRARY_DEBUG}") +endif() if(GMOCK_FOUND) set(GMOCK_INCLUDE_DIRS ${GMOCK_INCLUDE_DIR}) diff --git a/cmake/FindLZ4.cmake b/cmake/FindLZ4.cmake index ec854c6b18..5b148cb64e 100644 --- a/cmake/FindLZ4.cmake +++ b/cmake/FindLZ4.cmake @@ -95,6 +95,8 @@ include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LZ4 DEFAULT_MSG LZ4_LIBRARY LZ4_INCLUDE_DIR LZ4_VERSION) +set(LZ4_INCLUDE_DIR ${LZ4_INCLUDE_DIR} CACHE PATH "LZ4 include dir hint") +set(LZ4_LIBRARY ${LZ4_LIBRARY} CACHE FILEPATH "LZ4 library path hint") mark_as_advanced(LZ4_INCLUDE_DIR LZ4_LIBRARY) set(LZ4_LIBRARIES ${LZ4_LIBRARY}) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index b61ba11466..59bfa27c58 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate scriptscontainer utilpackage serialization omwscriptsparser + luastate scriptscontainer utilpackage serialization configuration ) add_component_dir (settings @@ -37,7 +37,7 @@ add_component_dir (settings ) add_component_dir (bsa - bsa_file compressedbsafile memorystream + bsa_file compressedbsafile ) add_component_dir (vfs @@ -55,13 +55,13 @@ add_component_dir (shader add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller - lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer + lightmanager lightutil positionattitudetransform workqueue pathgridutil waterutil writescene serialize optimizer actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt - screencapture + screencapture depth ) add_component_dir (nif - controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream + controlled effect niftypes record controller extra node record_ptr data niffile property nifkey base nifstream physics ) add_component_dir (nifosg @@ -93,6 +93,7 @@ add_component_dir (esmterrain add_component_dir (misc constants utf8stream stringops resourcehelpers rng messageformatparser weakcache thread + compression osguservalues errorMarker ) add_component_dir (debug @@ -105,7 +106,7 @@ IF(NOT WIN32 AND NOT APPLE) ENDIF() add_component_dir (files linuxpath androidpath windowspath macospath fixedpath multidircollection collections configurationmanager - lowlevelfile constrainedfilestream memorystream configfileparser + lowlevelfile constrainedfilestream memorystream hash configfileparser ) add_component_dir (compiler @@ -145,7 +146,7 @@ add_component_dir (fontloader ) add_component_dir (sdlutil - gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager + gl4es_init sdlgraphicswindow imagetosurface sdlinputwrapper sdlvideowrapper events sdlcursormanager sdlmappings ) add_component_dir (version @@ -159,6 +160,12 @@ add_component_dir (fallback add_component_dir (queries query luabindings ) + +add_component_dir (lua_ui + widget widgetlist element content + text textedit window + ) + if(WIN32) add_component_dir (crashcatcher @@ -195,12 +202,24 @@ add_component_dir(detournavigator offmeshconnectionsmanager preparednavmeshdata navmeshcacheitem + navigatorutils ) add_component_dir(loadinglistener reporter ) +add_component_dir(sqlite3 + db + statement + transaction +) + +add_component_dir(esmloader + load + esmdata +) + set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) @@ -236,6 +255,8 @@ endif () include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) +find_package(SQLite3 REQUIRED) + add_library(components STATIC ${COMPONENT_FILES}) target_link_libraries(components @@ -262,12 +283,15 @@ target_link_libraries(components ${SDL2_LIBRARIES} ${OPENGL_gl_LIBRARY} ${MyGUI_LIBRARIES} + ${LUA_LIBRARIES} LZ4::LZ4 RecastNavigation::DebugUtils RecastNavigation::Detour RecastNavigation::Recast Base64 + SQLite::SQLite3 + smhasher ) target_link_libraries(components ${BULLET_LIBRARIES}) @@ -348,3 +372,7 @@ endif(OSG_STATIC) if(USE_QT) set_property(TARGET components_qt PROPERTY AUTOMOC ON) endif(USE_QT) + +if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.16) + target_precompile_headers(components PRIVATE ${SOL_INCLUDE_DIR}/sol/sol.hpp) +endif () diff --git a/components/bsa/bsa_file.cpp b/components/bsa/bsa_file.cpp index ecbea5e7d9..e5b730b4c4 100644 --- a/components/bsa/bsa_file.cpp +++ b/components/bsa/bsa_file.cpp @@ -189,13 +189,6 @@ void BSAFile::readHeader() return left.offset < right.offset; }); - for (size_t i = 0; i < filenum; i++) - { - FileStruct& fs = mFiles[i]; - // Add the file name to the lookup - mLookup[fs.name()] = i; - } - mIsLoaded = true; } @@ -237,18 +230,6 @@ void Bsa::BSAFile::writeHeader() 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 -{ - auto it = mLookup.find(str); - if(it == mLookup.end()) - return -1; - - size_t res = it->second; - assert(res < mFiles.size()); - return static_cast(res); -} - /// Open an archive file. void BSAFile::open(const std::string &file) { @@ -274,27 +255,9 @@ void Bsa::BSAFile::close() mFiles.clear(); mStringBuf.clear(); - mLookup.clear(); mIsLoaded = false; } -Files::IStreamPtr BSAFile::getFile(const char *file) -{ - assert(file); - int i = getIndex(file); - if(i == -1) - fail("File not found: " + std::string(file)); - - const FileStruct &fs = mFiles[i]; - - return Files::openConstrainedFileStream (mFilename.c_str (), fs.offset, fs.fileSize); -} - -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) { if (!mIsLoaded) @@ -343,8 +306,6 @@ void Bsa::BSAFile::addFile(const std::string& filename, std::istream& file) 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 d30fc2feb0..f6c5d03139 100644 --- a/components/bsa/bsa_file.hpp +++ b/components/bsa/bsa_file.hpp @@ -27,9 +27,6 @@ #include #include #include -#include - -#include #include @@ -91,20 +88,6 @@ protected: /// Used for error messages std::string mFilename; - /// Case insensitive string comparison - struct iltstr - { - bool operator()(const std::string& s1, const std::string& s2) const - { return Misc::StringUtils::ciLess(s1, s2); } - }; - - /** A map used for fast file name lookup. The value is the index into - the files[] vector above. The iltstr ensures that file name - checks are case insensitive. - */ - typedef std::map Lookup; - Lookup mLookup; - /// Error handling [[noreturn]] void fail(const std::string &msg); @@ -112,10 +95,6 @@ protected: virtual void readHeader(); virtual void writeHeader(); - /// Get the index of a given file name, or -1 if not found - /// @note Thread safe. - int getIndex(const char *str) const; - public: /* ----------------------------------- * BSA management methods @@ -141,20 +120,13 @@ public: * ----------------------------------- */ - /// Check if a file exists - virtual bool exists(const char *file) const - { return getIndex(file) != -1; } - - /** Open a file contained in the archive. Throws an exception if the - file doesn't exist. - * @note Thread safe. - */ - virtual Files::IStreamPtr getFile(const char *file); - /** Open a file contained in the archive. * @note Thread safe. */ - virtual Files::IStreamPtr getFile(const FileStruct* file); + Files::IStreamPtr getFile(const FileStruct *file) + { + return Files::openConstrainedFileStream (mFilename.c_str (), file->offset, file->fileSize); + } virtual void addFile(const std::string& filename, std::istream& file); diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index f066489184..ae8209a39a 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -47,6 +47,7 @@ #include #include +#include namespace Bsa { @@ -286,7 +287,6 @@ void CompressedBSAFile::readHeader() mFiles[fileIndex].setNameInfos(mStringBuffOffset, &mStringBuf); - mLookup[reinterpret_cast(mStringBuf.data() + mStringBuffOffset)] = fileIndex; mStringBuffOffset += stringLength + 1u; } diff --git a/components/bsa/compressedbsafile.hpp b/components/bsa/compressedbsafile.hpp index ac6e6fdf78..f19815dcf3 100644 --- a/components/bsa/compressedbsafile.hpp +++ b/components/bsa/compressedbsafile.hpp @@ -26,6 +26,8 @@ #ifndef BSA_COMPRESSED_BSA_FILE_H #define BSA_COMPRESSED_BSA_FILE_H +#include + #include namespace Bsa @@ -92,8 +94,8 @@ namespace Bsa /// Read header information from the input source void readHeader() override; - Files::IStreamPtr getFile(const char* filePath) override; - Files::IStreamPtr getFile(const FileStruct* fileStruct) override; + Files::IStreamPtr getFile(const char* filePath); + Files::IStreamPtr getFile(const FileStruct* fileStruct); void addFile(const std::string& filename, std::istream& file) override; }; } diff --git a/components/bsa/memorystream.cpp b/components/bsa/memorystream.cpp deleted file mode 100644 index 34e98e6b68..0000000000 --- a/components/bsa/memorystream.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - OpenMW - The completely unofficial reimplementation of Morrowind - Copyright (C) 2008-2010 Nicolay Korslund - Email: < korslund@gmail.com > - WWW: http://openmw.sourceforge.net/ - - This file (memorystream.cpp) is part of the OpenMW package. - - OpenMW is distributed as free software: you can redistribute it - and/or modify it under the terms of the GNU General Public License - version 3, as published by the Free Software Foundation. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - version 3 along with this program. If not, see - http://www.gnu.org/licenses/ . - - Compressed BSA upgrade added by Azdul 2019 - - */ -#include "memorystream.hpp" - - -namespace Bsa -{ -MemoryInputStreamBuf::MemoryInputStreamBuf(size_t bufferSize) : mBufferPtr(bufferSize) -{ - this->setg(mBufferPtr.data(), mBufferPtr.data(), mBufferPtr.data() + bufferSize); -} - -char* MemoryInputStreamBuf::getRawData() { - return mBufferPtr.data(); -} - -MemoryInputStream::MemoryInputStream(size_t bufferSize) : - MemoryInputStreamBuf(bufferSize), - std::istream(static_cast(this)) { - -} - -char* MemoryInputStream::getRawData() { - return MemoryInputStreamBuf::getRawData(); -} -} diff --git a/components/bsa/memorystream.hpp b/components/bsa/memorystream.hpp index d168e93d65..5aae448299 100644 --- a/components/bsa/memorystream.hpp +++ b/components/bsa/memorystream.hpp @@ -28,22 +28,10 @@ #include #include +#include namespace Bsa { -/** -Class used internally by MemoryInputStream. -*/ -class MemoryInputStreamBuf : public std::streambuf { - -public: - explicit MemoryInputStreamBuf(size_t bufferSize); - virtual char* getRawData(); -private: - //correct call to delete [] on C++ 11 - std::vector mBufferPtr; -}; - /** Class replaces Ogre memory streams without introducing any new external dependencies beyond standard library. @@ -52,10 +40,18 @@ private: Memory buffer is freed once the class instance is destroyed. */ -class MemoryInputStream : virtual MemoryInputStreamBuf, std::istream { +class MemoryInputStream : private std::vector, public virtual Files::MemBuf, public std::istream { public: - explicit MemoryInputStream(size_t bufferSize); - char* getRawData() override; + explicit MemoryInputStream(size_t bufferSize) + : std::vector(bufferSize) + , Files::MemBuf(this->data(), this->size()) + , std::istream(static_cast(this)) + {} + + char* getRawData() + { + return this->data(); + } }; } diff --git a/components/bullethelpers/collisionobject.hpp b/components/bullethelpers/collisionobject.hpp new file mode 100644 index 0000000000..0951c2af38 --- /dev/null +++ b/components/bullethelpers/collisionobject.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_COLLISIONOBJECT_H +#define OPENMW_COMPONENTS_BULLETHELPERS_COLLISIONOBJECT_H + +#include + +#include +#include +#include + +#include + +namespace BulletHelpers +{ + inline std::unique_ptr makeCollisionObject(btCollisionShape* shape, + const btVector3& position, const btQuaternion& rotation) + { + std::unique_ptr result = std::make_unique(); + result->setCollisionShape(shape); + result->setWorldTransform(btTransform(rotation, position)); + return result; + } +} + +#endif diff --git a/components/bullethelpers/heightfield.hpp b/components/bullethelpers/heightfield.hpp new file mode 100644 index 0000000000..6b1f0ccc2e --- /dev/null +++ b/components/bullethelpers/heightfield.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_BULLETHELPERS_HEIGHTFIELD_H +#define OPENMW_COMPONENTS_BULLETHELPERS_HEIGHTFIELD_H + +#include + +namespace BulletHelpers +{ + inline btVector3 getHeightfieldShift(int x, int y, int size, float minHeight, float maxHeight) + { + return btVector3((x + 0.5f) * size, (y + 0.5f) * size, (maxHeight + minHeight) * 0.5f); + } +} + +#endif diff --git a/components/compiler/context.hpp b/components/compiler/context.hpp index 399e8125bb..d3caba7c53 100644 --- a/components/compiler/context.hpp +++ b/components/compiler/context.hpp @@ -42,9 +42,6 @@ namespace Compiler virtual bool isId (const std::string& name) const = 0; ///< Does \a name match an ID, that can be referenced? - - virtual bool isJournalId (const std::string& name) const = 0; - ///< Does \a name match a journal ID? }; } diff --git a/components/compiler/declarationparser.cpp b/components/compiler/declarationparser.cpp index 1c64aaadee..f29fe820c1 100644 --- a/components/compiler/declarationparser.cpp +++ b/components/compiler/declarationparser.cpp @@ -90,6 +90,16 @@ bool Compiler::DeclarationParser::parseSpecial (int code, const TokenLoc& loc, S return Parser::parseSpecial (code, loc, scanner); } +bool Compiler::DeclarationParser::parseInt(int value, const TokenLoc& loc, Scanner& scanner) +{ + if(mState == State_Name) + { + // Allow integers to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } + return Parser::parseInt(value, loc, scanner); +} + void Compiler::DeclarationParser::reset() { mState = State_Begin; diff --git a/components/compiler/declarationparser.hpp b/components/compiler/declarationparser.hpp index c04f1dc268..d08509fe50 100644 --- a/components/compiler/declarationparser.hpp +++ b/components/compiler/declarationparser.hpp @@ -35,6 +35,10 @@ namespace Compiler ///< Handle a special character token. /// \return fetch another token? + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. + /// \return fetch another token? + void reset() override; }; diff --git a/components/compiler/discardparser.cpp b/components/compiler/discardparser.cpp index 0e7c4718cb..0a714d4eb6 100644 --- a/components/compiler/discardparser.cpp +++ b/components/compiler/discardparser.cpp @@ -12,7 +12,7 @@ namespace Compiler bool DiscardParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState || mState==MinusState) + if (mState==StartState || mState==MinusState) { if (isEmpty()) mTokenLoc = loc; @@ -26,7 +26,7 @@ namespace Compiler bool DiscardParser::parseFloat (float value, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState || mState==MinusState) + if (mState==StartState || mState==MinusState) { if (isEmpty()) mTokenLoc = loc; @@ -41,7 +41,7 @@ namespace Compiler bool DiscardParser::parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState) + if (mState==StartState) { if (isEmpty()) mTokenLoc = loc; @@ -55,18 +55,7 @@ namespace Compiler bool DiscardParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) { - if (code==Scanner::S_comma && mState==StartState) - { - if (isEmpty()) - mTokenLoc = loc; - - start(); - - mState = CommaState; - return true; - } - - if (code==Scanner::S_minus && (mState==StartState || mState==CommaState)) + if (code==Scanner::S_minus && mState==StartState) { if (isEmpty()) mTokenLoc = loc; diff --git a/components/compiler/discardparser.hpp b/components/compiler/discardparser.hpp index 15e06756e2..5286676654 100644 --- a/components/compiler/discardparser.hpp +++ b/components/compiler/discardparser.hpp @@ -11,7 +11,7 @@ namespace Compiler { enum State { - StartState, CommaState, MinusState + StartState, MinusState }; State mState; diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index 2d525e6f8c..668946f839 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -244,7 +244,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackInt (value, loc); return false; } @@ -267,7 +266,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackFloat (value, loc); return false; } @@ -343,7 +341,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackName (name, loc); return false; } @@ -452,7 +449,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackKeyword (keyword, loc); return false; } @@ -487,22 +483,6 @@ namespace Compiler return Parser::parseSpecial (code, loc, scanner); } - if (code==Scanner::S_comma) - { - mTokenLoc = loc; - - if (mFirst) - { - // leading comma - mFirst = false; - return true; - } - - // end marker - scanner.putbackSpecial (code, loc); - return false; - } - mFirst = false; if (code==Scanner::S_newline) @@ -539,7 +519,6 @@ namespace Compiler } else { - // no comma was used between arguments scanner.putbackSpecial (code, loc); return false; } @@ -632,7 +611,7 @@ namespace Compiler } int ExprParser::parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword) + std::vector& code, int ignoreKeyword, bool expectNames) { bool optional = false; int optionalCount = 0; @@ -717,6 +696,8 @@ namespace Compiler if (optional) parser.setOptional (true); + if(expectNames) + scanner.enableExpectName(); scanner.scan (parser); diff --git a/components/compiler/exprparser.hpp b/components/compiler/exprparser.hpp index 2f3eaa8a9f..42739658ec 100644 --- a/components/compiler/exprparser.hpp +++ b/components/compiler/exprparser.hpp @@ -96,7 +96,7 @@ namespace Compiler /// \return Type ('l': integer, 'f': float) int parseArguments (const std::string& arguments, Scanner& scanner, - std::vector& code, int ignoreKeyword = -1); + std::vector& code, int ignoreKeyword = -1, bool expectNames = false); ///< Parse sequence of arguments specified by \a arguments. /// \param arguments Uses ScriptArgs typedef /// \see Compiler::ScriptArgs diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 3dfcadab10..64133bee84 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -553,7 +553,7 @@ namespace Compiler extensions.registerFunction("getpos",'f',"c",opcodeGetPos,opcodeGetPosExplicit); extensions.registerFunction("getstartingpos",'f',"c",opcodeGetStartingPos,opcodeGetStartingPosExplicit); extensions.registerInstruction("position","ffffz",opcodePosition,opcodePositionExplicit); - extensions.registerInstruction("positioncell","ffffcX",opcodePositionCell,opcodePositionCellExplicit); + extensions.registerInstruction("positioncell","ffffczz",opcodePositionCell,opcodePositionCellExplicit); extensions.registerInstruction("placeitemcell","ccffffX",opcodePlaceItemCell); extensions.registerInstruction("placeitem","cffffX",opcodePlaceItem); extensions.registerInstruction("placeatpc","clflX",opcodePlaceAtPc); diff --git a/components/compiler/fileparser.cpp b/components/compiler/fileparser.cpp index c7459c2ae7..9d5c02785e 100644 --- a/components/compiler/fileparser.cpp +++ b/components/compiler/fileparser.cpp @@ -121,11 +121,6 @@ namespace Compiler return false; } } - else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState)) - { - // ignoring comma (for now) - return true; - } return Parser::parseSpecial (code, loc, scanner); } diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 77afaee8bd..2e398618a3 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -67,6 +67,11 @@ namespace Compiler parseExpression (scanner, loc); return true; } + else if (mState == SetState) + { + // Allow ints to be used as variable names + return parseName(loc.mLiteral, loc, scanner); + } return Parser::parseInt (value, loc, scanner); } @@ -131,7 +136,7 @@ namespace Compiler return false; } - if (mState==MessageState || mState==MessageCommaState) + if (mState==MessageState) { GetArgumentsFromMessageFormat processor; processor.process(name); @@ -140,7 +145,7 @@ namespace Compiler if (!arguments.empty()) { mExprParser.reset(); - mExprParser.parseArguments (arguments, scanner, mCode); + mExprParser.parseArguments (arguments, scanner, mCode, -1, true); } mName = name; @@ -150,7 +155,7 @@ namespace Compiler return true; } - if (mState==MessageButtonState || mState==MessageButtonCommaState) + if (mState==MessageButtonState) { Generator::pushString (mCode, mLiterals, name); mState = MessageButtonState; @@ -193,7 +198,7 @@ namespace Compiler bool LineParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner) { - if (mState==MessageState || mState==MessageCommaState) + if (mState==MessageState) { if (const Extensions *extensions = getContext().getExtensions()) { @@ -441,12 +446,6 @@ namespace Compiler if (code==Scanner::S_newline && (mState==EndState || mState==BeginState)) return false; - if (code==Scanner::S_comma && mState==MessageState) - { - mState = MessageCommaState; - return true; - } - if (code==Scanner::S_ref && mState==SetPotentialMemberVarState) { getErrorHandler().warning ("Stray explicit reference", loc); @@ -474,12 +473,6 @@ namespace Compiler return false; } - if (code==Scanner::S_comma && mState==MessageButtonState) - { - mState = MessageButtonCommaState; - return true; - } - if (code==Scanner::S_member && mState==SetPotentialMemberVarState) { mState = SetMemberVarState; diff --git a/components/compiler/lineparser.hpp b/components/compiler/lineparser.hpp index c434792d18..2a0e5d6630 100644 --- a/components/compiler/lineparser.hpp +++ b/components/compiler/lineparser.hpp @@ -24,7 +24,7 @@ namespace Compiler BeginState, SetState, SetLocalVarState, SetGlobalVarState, SetPotentialMemberVarState, SetMemberVarState, SetMemberVarState2, - MessageState, MessageCommaState, MessageButtonState, MessageButtonCommaState, + MessageState, MessageButtonState, EndState, PotentialExplicitState, ExplicitState, MemberState }; diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 1054e2e269..6cf2d6e7c0 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -130,7 +130,8 @@ namespace Compiler { bool cont = false; - if (scanInt (c, parser, cont)) + bool scanned = mExpectName ? scanName(c, parser, cont) : scanInt(c, parser, cont); + if (scanned) { mLoc.mLiteral.clear(); return cont; @@ -387,6 +388,8 @@ namespace Compiler bool Scanner::scanSpecial (MultiChar& c, Parser& parser, bool& cont) { + bool expectName = mExpectName; + mExpectName = false; int special = -1; if (c=='\n') @@ -528,8 +531,6 @@ namespace Compiler else special = S_cmpGT; } - else if (c==',') - special = S_comma; else if (c=='+') special = S_plus; else if (c=='*') @@ -541,9 +542,8 @@ namespace Compiler if (special==S_newline) mLoc.mLiteral = ""; - else if (mExpectName && (special == S_member || special == S_minus)) + else if (expectName && (special == S_member || special == S_minus)) { - mExpectName = false; bool tolerant = mTolerantNames; mTolerantNames = true; bool out = scanName(c, parser, cont); diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 8ee2672132..7a77811f2a 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -63,7 +63,7 @@ namespace Compiler bool isWhitespace() { - return (mData[0]==' ' || mData[0]=='\t') && mData[1]==0 && mData[2]==0 && mData[3]==0; + return (mData[0]==' ' || mData[0]=='\t' || mData[0]==',') && mData[1]==0 && mData[2]==0 && mData[3]==0; } bool isDigit() @@ -214,7 +214,6 @@ namespace Compiler S_open, S_close, S_cmpEQ, S_cmpNE, S_cmpLT, S_cmpLE, S_cmpGT, S_cmpGE, S_plus, S_minus, S_mult, S_div, - S_comma, S_ref, S_member }; diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index d9c3c04947..a6423211c2 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -13,7 +13,7 @@ namespace Compiler { StringParser::StringParser (ErrorHandler& errorHandler, const Context& context, Literals& literals) - : Parser (errorHandler, context), mLiterals (literals), mState (StartState), mSmashCase (false), mDiscard (false) + : Parser (errorHandler, context), mLiterals (literals), mSmashCase (false), mDiscard (false) { } @@ -21,23 +21,18 @@ namespace Compiler bool StringParser::parseName (const std::string& name, const TokenLoc& loc, Scanner& scanner) { - if (mState==StartState || mState==CommaState) - { - start(); - mTokenLoc = loc; - - if (!mDiscard) - { - if (mSmashCase) - Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name)); - else - Generator::pushString (mCode, mLiterals, name); - } + start(); + mTokenLoc = loc; - return false; + if (!mDiscard) + { + if (mSmashCase) + Generator::pushString (mCode, mLiterals, Misc::StringUtils::lowerCase (name)); + else + Generator::pushString (mCode, mLiterals, name); } - return Parser::parseName (name, loc, scanner); + return false; } bool StringParser::parseKeyword (int keyword, const TokenLoc& loc, Scanner& scanner) @@ -75,15 +70,10 @@ namespace Compiler return Parser::parseKeyword (keyword, loc, scanner); } - bool StringParser::parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) + bool StringParser::parseInt (int value, const TokenLoc& loc, Scanner& scanner) { - if (code==Scanner::S_comma && mState==StartState) - { - mState = CommaState; - return true; - } - - return Parser::parseSpecial (code, loc, scanner); + reportWarning("Treating integer argument as a string", loc); + return parseName(loc.mLiteral, loc, scanner); } void StringParser::append (std::vector& code) @@ -93,7 +83,6 @@ namespace Compiler void StringParser::reset() { - mState = StartState; mCode.clear(); mSmashCase = false; mTokenLoc = TokenLoc(); diff --git a/components/compiler/stringparser.hpp b/components/compiler/stringparser.hpp index 1976628360..cdc7c676a7 100644 --- a/components/compiler/stringparser.hpp +++ b/components/compiler/stringparser.hpp @@ -14,13 +14,7 @@ namespace Compiler class StringParser : public Parser { - enum State - { - StartState, CommaState - }; - Literals& mLiterals; - State mState; std::vector mCode; bool mSmashCase; TokenLoc mTokenLoc; @@ -39,8 +33,8 @@ namespace Compiler ///< Handle a keyword token. /// \return fetch another token? - bool parseSpecial (int code, const TokenLoc& loc, Scanner& scanner) override; - ///< Handle a special character token. + bool parseInt (int value, const TokenLoc& loc, Scanner& scanner) override; + ///< Handle an int token. /// \return fetch another token? void append (std::vector& code); diff --git a/components/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 690f968142..199799025a 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -2,6 +2,7 @@ #include "esmfile.hpp" #include +#include #include #include @@ -9,9 +10,10 @@ #include -ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon) : +ContentSelectorModel::ContentModel::ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts) : QAbstractTableModel(parent), mWarningIcon(warningIcon), + mShowOMWScripts(showOMWScripts), mMimeType ("application/omwcontent"), mMimeTypes (QStringList() << mMimeType), mColumnCount (1), @@ -416,7 +418,10 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) QDir dir(path); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; + if (mShowOMWScripts) + filters << "*.omwscripts"; dir.setNameFilters(filters); + dir.setSorting(QDir::Name); for (const QString &path2 : dir.entryList()) { @@ -425,6 +430,19 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) if (item(info.fileName())) continue; + // Enabled by default in system openmw.cfg; shouldn't be shown in content list. + if (info.fileName().compare("builtin.omwscripts", Qt::CaseInsensitive) == 0) + continue; + + if (info.fileName().endsWith(".omwscripts", Qt::CaseInsensitive)) + { + EsmFile *file = new EsmFile(path2); + file->setDate(info.lastModified()); + file->setFilePath(info.absoluteFilePath()); + addFile(file); + continue; + } + try { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder = @@ -462,8 +480,6 @@ void ContentSelectorModel::ContentModel::addFiles(const QString &path) } } - - sortFiles(); } void ContentSelectorModel::ContentModel::clearFiles() @@ -492,41 +508,37 @@ QStringList ContentSelectorModel::ContentModel::gameFiles() const void ContentSelectorModel::ContentModel::sortFiles() { - //first, sort the model such that all dependencies are ordered upstream (gamefile) first. - bool movedFiles = true; - int fileCount = mFiles.size(); - + emit layoutAboutToBeChanged(); //Dependency sort - //iterate until no sorting of files occurs - while (movedFiles) + std::unordered_set moved; + for(int i = mFiles.size() - 1; i > 0;) { - movedFiles = false; - //iterate each file, obtaining a reference to it's gamefiles list - for (int i = 0; i < fileCount; i++) + const auto file = mFiles.at(i); + if(moved.find(file) == moved.end()) { - QModelIndex idx1 = index (i, 0, QModelIndex()); - const QStringList &gamefiles = mFiles.at(i)->gameFiles(); - //iterate each file after the current file, verifying that none of it's - //dependencies appear. - for (int j = i + 1; j < fileCount; j++) + int index = -1; + for(int j = 0; j < i; ++j) { - if (gamefiles.contains(mFiles.at(j)->fileName(), Qt::CaseInsensitive) - || (!mFiles.at(i)->isGameFile() && gamefiles.isEmpty() - && mFiles.at(j)->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files + const QStringList& gameFiles = mFiles.at(j)->gameFiles(); + if(gameFiles.contains(file->fileName(), Qt::CaseInsensitive) + || (!mFiles.at(j)->isGameFile() && gameFiles.isEmpty() + && file->fileName().compare("Morrowind.esm", Qt::CaseInsensitive) == 0)) // Hack: implicit dependency on Morrowind.esm for dependency-less files { - mFiles.move(j, i); - - QModelIndex idx2 = index (j, 0, QModelIndex()); - - emit dataChanged (idx1, idx2); - - movedFiles = true; + index = j; + break; } } - if (movedFiles) - break; + if(index >= 0) + { + mFiles.move(i, index); + moved.insert(file); + continue; + } } + --i; + moved.clear(); } + emit layoutChanged(); } bool ContentSelectorModel::ContentModel::isChecked(const QString& filepath) const diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index d245a0dcbf..4bbe73b427 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -23,7 +23,7 @@ namespace ContentSelectorModel { Q_OBJECT public: - explicit ContentModel(QObject *parent, QIcon warningIcon); + explicit ContentModel(QObject *parent, QIcon warningIcon, bool showOMWScripts); ~ContentModel(); void setEncoding(const QString &encoding); @@ -44,6 +44,7 @@ namespace ContentSelectorModel bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; void addFiles(const QString &path); + void sortFiles(); void clearFiles(); QModelIndex indexFromItem(const EsmFile *item) const; @@ -68,8 +69,6 @@ namespace ContentSelectorModel void addFile(EsmFile *file); - void sortFiles(); - /// Checks a specific plug-in for load order errors /// \return all errors found for specific plug-in QList checkForLoadOrderErrors(const EsmFile *file, int row) const; @@ -84,6 +83,7 @@ namespace ContentSelectorModel QSet mPluginsWithLoadOrderError; QString mEncoding; QIcon mWarningIcon; + bool mShowOMWScripts; public: diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index d7996dfae3..ef925148ab 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -10,21 +10,21 @@ #include #include -ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent) : +ContentSelectorView::ContentSelector::ContentSelector(QWidget *parent, bool showOMWScripts) : QObject(parent) { ui.setupUi(parent); ui.addonView->setDragDropMode(QAbstractItemView::InternalMove); - buildContentModel(); + buildContentModel(showOMWScripts); buildGameFileView(); buildAddonView(); } -void ContentSelectorView::ContentSelector::buildContentModel() +void ContentSelectorView::ContentSelector::buildContentModel(bool showOMWScripts) { QIcon warningIcon(ui.addonView->style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(QSize(16, 15))); - mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon); + mContentModel = new ContentSelectorModel::ContentModel(this, warningIcon, showOMWScripts); } void ContentSelectorView::ContentSelector::buildGameFileView() @@ -173,6 +173,11 @@ void ContentSelectorView::ContentSelector::addFiles(const QString &path) mContentModel->checkForLoadOrderErrors(); } +void ContentSelectorView::ContentSelector::sortFiles() +{ + mContentModel->sortFiles(); +} + void ContentSelectorView::ContentSelector::clearFiles() { mContentModel->clearFiles(); diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index cda68fa1b7..b40675bedc 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -23,11 +23,12 @@ namespace ContentSelectorView public: - explicit ContentSelector(QWidget *parent = nullptr); + explicit ContentSelector(QWidget *parent = nullptr, bool showOMWScripts = false); QString currentFile() const; void addFiles(const QString &path); + void sortFiles(); void clearFiles(); void setProfileContent (const QStringList &fileList); @@ -56,7 +57,7 @@ namespace ContentSelectorView Ui::ContentSelector ui; - void buildContentModel(); + void buildContentModel(bool showOMWScripts); void buildGameFileView(); void buildAddonView(); void buildContextMenu(); diff --git a/components/crashcatcher/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp index 86571e1e3a..c828e1ca81 100644 --- a/components/crashcatcher/crashcatcher.cpp +++ b/components/crashcatcher/crashcatcher.cpp @@ -56,8 +56,6 @@ static const char exec_err[] = "!!! Failed to exec debug process\n"; static char argv0[PATH_MAX]; -static char altstack[SIGSTKSZ]; - static struct { int signum; @@ -475,9 +473,10 @@ int crashCatcherInstallHandlers(int argc, char **argv, int num_signals, int *sig /* Set an alternate signal stack so SIGSEGVs caused by stack overflows * still run */ + static char* altstack = new char [SIGSTKSZ]; altss.ss_sp = altstack; altss.ss_flags = 0; - altss.ss_size = sizeof(altstack); + altss.ss_size = SIGSTKSZ; sigaltstack(&altss, nullptr); memset(&sa, 0, sizeof(sa)); diff --git a/components/debug/debuglog.hpp b/components/debug/debuglog.hpp index 0da5b9cbdd..96b9d798e9 100644 --- a/components/debug/debuglog.hpp +++ b/components/debug/debuglog.hpp @@ -4,8 +4,6 @@ #include #include -#include - namespace Debug { enum Level @@ -29,25 +27,29 @@ class Log std::unique_lock mLock; public: - // Locks a global lock while the object is alive - Log(Debug::Level level) : - mLock(sLock), - mLevel(level) + explicit Log(Debug::Level level) + : mShouldLog(level <= Debug::CurrentDebugLevel) { + // No need to hold the lock if there will be no logging anyway + if (!mShouldLog) + return; + + // Locks a global lock while the object is alive + mLock = std::unique_lock(sLock); + // If the app has no logging system enabled, log level is not specified. // Show all messages without marker - we just use the plain cout in this case. if (Debug::CurrentDebugLevel == Debug::NoLevel) return; - if (mLevel <= Debug::CurrentDebugLevel) - std::cout << static_cast(mLevel); + std::cout << static_cast(level); } // Perfect forwarding wrappers to give the chain of objects to cout template Log& operator<<(T&& rhs) { - if (mLevel <= Debug::CurrentDebugLevel) + if (mShouldLog) std::cout << std::forward(rhs); return *this; @@ -55,12 +57,12 @@ public: ~Log() { - if (mLevel <= Debug::CurrentDebugLevel) + if (mShouldLog) std::cout << std::endl; } private: - Debug::Level mLevel; + const bool mShouldLog; }; #endif diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 5c88d0cfa2..583fd1162a 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -311,12 +311,7 @@ namespace DetourNavigator if (recastMesh != nullptr) { - Version navMeshVersion; - { - const auto locked = navMeshCacheItem->lockConst(); - navMeshVersion.mGeneration = locked->getGeneration(); - navMeshVersion.mRevision = locked->getNavMeshRevision(); - } + const Version navMeshVersion = navMeshCacheItem->lockConst()->getVersion(); mRecastMeshManager.get().reportNavMeshChange(job.mChangedTile, Version {recastMesh->getGeneration(), recastMesh->getRevision()}, navMeshVersion); @@ -339,13 +334,13 @@ namespace DetourNavigator using FloatMs = std::chrono::duration; - const auto locked = navMeshCacheItem->lockConst(); + const Version version = navMeshCacheItem->lockConst()->getVersion(); Log(Debug::Debug) << std::fixed << std::setprecision(2) << "Cache updated for agent=(" << job.mAgentHalfExtents << ")" << " tile=" << job.mChangedTile << " status=" << status << - " generation=" << locked->getGeneration() << - " revision=" << locked->getNavMeshRevision() << + " generation=" << version.mGeneration << + " revision=" << version.mRevision << " time=" << std::chrono::duration_cast(finish - start).count() << "ms" << " thread=" << std::this_thread::get_id(); @@ -409,7 +404,7 @@ namespace DetourNavigator } if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile) writeToFile(*recastMesh, mSettings.get().mRecastMeshPathPrefix + std::to_string(job.mChangedTile.x()) - + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision); + + "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision, mSettings); if (mSettings.get().mEnableWriteNavMeshToFile) if (const auto shared = job.mNavMeshCacheItem.lock()) writeToFile(shared->lockConst()->getImpl(), mSettings.get().mNavMeshPathPrefix, navMeshRevision); diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index 19b87aa820..a3c7f4b172 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -3,9 +3,8 @@ namespace DetourNavigator { - CachedRecastMeshManager::CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, - std::size_t generation) - : mImpl(settings, bounds, generation) + CachedRecastMeshManager::CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation) + : mImpl(bounds, generation) {} bool CachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, @@ -13,7 +12,7 @@ namespace DetourNavigator { if (!mImpl.addObject(id, shape, transform, areaType)) return false; - mCached.lock()->reset(); + mOutdatedCache = true; return true; } @@ -21,7 +20,7 @@ namespace DetourNavigator { if (!mImpl.updateObject(id, transform, areaType)) return false; - mCached.lock()->reset(); + mOutdatedCache = true; return true; } @@ -29,52 +28,60 @@ namespace DetourNavigator { auto object = mImpl.removeObject(id); if (object) - mCached.lock()->reset(); + mOutdatedCache = true; return object; } - bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const osg::Vec3f& shift) + bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - if (!mImpl.addWater(cellPosition, cellSize, shift)) + if (!mImpl.addWater(cellPosition, cellSize, level)) return false; - mCached.lock()->reset(); + mOutdatedCache = true; return true; } - std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto water = mImpl.removeWater(cellPosition); if (water) - mCached.lock()->reset(); + mOutdatedCache = true; return water; } bool CachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, - const osg::Vec3f& shift, const HeightfieldShape& shape) + const HeightfieldShape& shape) { - if (!mImpl.addHeightfield(cellPosition, cellSize, shift, shape)) + if (!mImpl.addHeightfield(cellPosition, cellSize, shape)) return false; - mCached.lock()->reset(); + mOutdatedCache = true; return true; } - std::optional CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + std::optional CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) { - const auto cell = mImpl.removeHeightfield(cellPosition); - if (cell) - mCached.lock()->reset(); - return cell; + const auto heightfield = mImpl.removeHeightfield(cellPosition); + if (heightfield) + mOutdatedCache = true; + return heightfield; } std::shared_ptr CachedRecastMeshManager::getMesh() { - std::shared_ptr cached = *mCached.lock(); - if (cached != nullptr) - return cached; - cached = mImpl.getMesh(); - *mCached.lock() = cached; - return cached; + bool outdated = true; + if (!mOutdatedCache.compare_exchange_strong(outdated, false)) + { + std::shared_ptr cached = getCachedMesh(); + if (cached != nullptr) + return cached; + } + std::shared_ptr mesh = mImpl.getMesh(); + *mCached.lock() = mesh; + return mesh; + } + + std::shared_ptr CachedRecastMeshManager::getCachedMesh() const + { + return *mCached.lockConst(); } bool CachedRecastMeshManager::isEmpty() const diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index b506f807fa..eb18519891 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -7,12 +7,14 @@ #include +#include + namespace DetourNavigator { class CachedRecastMeshManager { public: - CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); + explicit CachedRecastMeshManager(const TileBounds& bounds, std::size_t generation); bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); @@ -21,17 +23,18 @@ namespace DetourNavigator std::optional removeObject(const ObjectId id); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); - std::optional removeWater(const osg::Vec2i& cellPosition); + std::optional removeWater(const osg::Vec2i& cellPosition); - bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); - std::optional removeHeightfield(const osg::Vec2i& cellPosition); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); std::shared_ptr getMesh(); + std::shared_ptr getCachedMesh() const; + bool isEmpty() const; void reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion); @@ -41,6 +44,7 @@ namespace DetourNavigator private: RecastMeshManager mImpl; Misc::ScopeGuarded> mCached; + std::atomic_bool mOutdatedCache {true}; }; } diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp index 4cb5b248b0..07832d748f 100644 --- a/components/detournavigator/debug.cpp +++ b/components/detournavigator/debug.cpp @@ -1,6 +1,8 @@ #include "debug.hpp" #include "exceptions.hpp" #include "recastmesh.hpp" +#include "settings.hpp" +#include "settingsutils.hpp" #include @@ -9,7 +11,7 @@ namespace DetourNavigator { - void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision) + void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision, const Settings& settings) { const auto path = pathPrefix + "recastmesh" + revision + ".obj"; boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out); @@ -17,20 +19,14 @@ namespace DetourNavigator throw NavigatorException("Open file failed: " + path); file.exceptions(std::ios::failbit | std::ios::badbit); file.precision(std::numeric_limits::max_exponent10); - std::size_t count = 0; - for (float v : recastMesh.getMesh().getVertices()) + std::vector vertices = recastMesh.getMesh().getVertices(); + for (std::size_t i = 0; i < vertices.size(); i += 3) { - if (count % 3 == 0) - { - if (count != 0) - file << '\n'; - file << 'v'; - } - file << ' ' << v; - ++count; + file << "v " << toNavMeshCoordinates(settings, vertices[i]) << ' ' + << toNavMeshCoordinates(settings, vertices[i + 2]) << ' ' + << toNavMeshCoordinates(settings, vertices[i + 1]) << '\n'; } - file << '\n'; - count = 0; + std::size_t count = 0; for (int v : recastMesh.getMesh().getIndices()) { if (count % 3 == 0) diff --git a/components/detournavigator/debug.hpp b/components/detournavigator/debug.hpp index 2128f96be4..a868ac2a2e 100644 --- a/components/detournavigator/debug.hpp +++ b/components/detournavigator/debug.hpp @@ -7,16 +7,8 @@ #include #include -#include - -#include -#include -#include -#include -#include -#include + #include -#include class dtNavMesh; @@ -48,8 +40,9 @@ namespace DetourNavigator } class RecastMesh; + struct Settings; - void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision); + void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision, const Settings& settings); void writeToFile(const dtNavMesh& navMesh, const std::string& pathPrefix, const std::string& revision); } diff --git a/components/detournavigator/findsmoothpath.cpp b/components/detournavigator/findsmoothpath.cpp index 14fe696f10..cbaf12305c 100644 --- a/components/detournavigator/findsmoothpath.cpp +++ b/components/detournavigator/findsmoothpath.cpp @@ -7,12 +7,16 @@ namespace DetourNavigator { - std::vector fixupCorridor(const std::vector& path, const std::vector& visited) + std::size_t fixupCorridor(std::vector& path, std::size_t pathSize, const std::vector& visited) { std::vector::const_reverse_iterator furthestVisited; // Find furthest common polygon. - const auto it = std::find_if(path.rbegin(), path.rend(), [&] (dtPolyRef pathValue) + const auto begin = path.begin(); + const auto end = path.begin() + pathSize; + const std::reverse_iterator rbegin(end); + const std::reverse_iterator rend(begin); + const auto it = std::find_if(rbegin, rend, [&] (dtPolyRef pathValue) { const auto it = std::find(visited.rbegin(), visited.rend(), pathValue); if (it == visited.rend()) @@ -22,48 +26,35 @@ namespace DetourNavigator }); // If no intersection found just return current path. - if (it == path.rend()) - return path; + if (it == rend) + return pathSize; const auto furthestPath = it.base() - 1; // Concatenate paths. // visited: a_1 ... a_n x b_1 ... b_n // furthestVisited ^ - // path: C x D - // ^ furthestPath + // path: C x D E + // ^ furthestPath ^ path.size() - (furthestVisited + 1 - visited.rbegin()) // result: x b_n ... b_1 D - std::vector result; - result.reserve(static_cast(furthestVisited - visited.rbegin()) - + static_cast(path.end() - furthestPath) - 1); - std::copy(visited.rbegin(), furthestVisited + 1, std::back_inserter(result)); - std::copy(furthestPath + 1, path.end(), std::back_inserter(result)); + const std::size_t required = static_cast(furthestVisited + 1 - visited.rbegin()); + const auto newEnd = std::copy(furthestPath + 1, std::min(begin + path.size(), end), begin + required); + std::copy(visited.rbegin(), furthestVisited + 1, begin); - return result; + return static_cast(newEnd - begin); } - // This function checks if the path has a small U-turn, that is, - // a polygon further in the path is adjacent to the first polygon - // in the path. If that happens, a shortcut is taken. - // This can happen if the target (T) location is at tile boundary, - // and we're (S) approaching it parallel to the tile edge. - // The choice at the vertex can be arbitrary, - // +---+---+ - // |:::|:::| - // +-S-+-T-+ - // |:::| | <-- the step can end up in here, resulting U-turn path. - // +---+---+ - std::vector fixupShortcuts(const std::vector& path, const dtNavMeshQuery& navQuery) + std::size_t fixupShortcuts(dtPolyRef* path, std::size_t pathSize, const dtNavMeshQuery& navQuery) { - if (path.size() < 3) - return path; + if (pathSize < 3) + return pathSize; // Get connected polygons const dtMeshTile* tile = nullptr; const dtPoly* poly = nullptr; if (dtStatusFailed(navQuery.getAttachedNavMesh()->getTileAndPolyByRef(path[0], &tile, &poly))) - return path; + return pathSize; const std::size_t maxNeis = 16; std::array neis; @@ -83,7 +74,7 @@ namespace DetourNavigator // in the path, short cut to that polygon directly. const std::size_t maxLookAhead = 6; std::size_t cut = 0; - for (std::size_t i = std::min(maxLookAhead, path.size()) - 1; i > 1 && cut == 0; i--) + for (std::size_t i = std::min(maxLookAhead, pathSize) - 1; i > 1 && cut == 0; i--) { for (std::size_t j = 0; j < nneis; j++) { @@ -95,18 +86,15 @@ namespace DetourNavigator } } if (cut <= 1) - return path; + return pathSize; - std::vector result; - const auto offset = cut - 1; - result.reserve(1 + path.size() - offset); - result.push_back(path.front()); - std::copy(path.begin() + std::ptrdiff_t(offset), path.end(), std::back_inserter(result)); - return result; + const std::ptrdiff_t offset = static_cast(cut) - 1; + std::copy(path + offset, path + pathSize, path); + return pathSize - offset; } std::optional getSteerTarget(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& startPos, - const osg::Vec3f& endPos, const float minTargetDist, const std::vector& path) + const osg::Vec3f& endPos, const float minTargetDist, const dtPolyRef* path, const std::size_t pathSize) { // Find steer target. SteerTarget result; @@ -115,8 +103,8 @@ namespace DetourNavigator std::array steerPathFlags; std::array steerPathPolys; int nsteerPath = 0; - const dtStatus status = navMeshQuery.findStraightPath(startPos.ptr(), endPos.ptr(), path.data(), - static_cast(path.size()), steerPath.data(), steerPathFlags.data(), steerPathPolys.data(), + const dtStatus status = navMeshQuery.findStraightPath(startPos.ptr(), endPos.ptr(), path, + static_cast(pathSize), steerPath.data(), steerPathFlags.data(), steerPathPolys.data(), &nsteerPath, maxSteerPoints); if (dtStatusFailed(status)) return std::nullopt; @@ -138,10 +126,10 @@ namespace DetourNavigator if (ns >= static_cast(nsteerPath)) return std::nullopt; - dtVcopy(result.steerPos.ptr(), &steerPath[ns * 3]); - result.steerPos.y() = startPos[1]; - result.steerPosFlag = steerPathFlags[ns]; - result.steerPosRef = steerPathPolys[ns]; + dtVcopy(result.mSteerPos.ptr(), &steerPath[ns * 3]); + result.mSteerPos.y() = startPos[1]; + result.mSteerPosFlag = steerPathFlags[ns]; + result.mSteerPosRef = steerPathPolys[ns]; return result; } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index 8eb0bf9f34..2e63340578 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -18,6 +18,7 @@ #include #include +#include class dtNavMesh; @@ -30,7 +31,7 @@ namespace DetourNavigator return (osg::Vec2f(v1.x(), v1.z()) - osg::Vec2f(v2.x(), v2.z())).length() < r; } - std::vector fixupCorridor(const std::vector& path, const std::vector& visited); + std::size_t fixupCorridor(std::vector& path, std::size_t pathSize, const std::vector& visited); // This function checks if the path has a small U-turn, that is, // a polygon further in the path is adjacent to the first polygon @@ -43,17 +44,17 @@ namespace DetourNavigator // +-S-+-T-+ // |:::| | <-- the step can end up in here, resulting U-turn path. // +---+---+ - std::vector fixupShortcuts(const std::vector& path, const dtNavMeshQuery& navQuery); + std::size_t fixupShortcuts(dtPolyRef* path, std::size_t pathSize, const dtNavMeshQuery& navQuery); struct SteerTarget { - osg::Vec3f steerPos; - unsigned char steerPosFlag; - dtPolyRef steerPosRef; + osg::Vec3f mSteerPos; + unsigned char mSteerPosFlag; + dtPolyRef mSteerPosRef; }; std::optional getSteerTarget(const dtNavMeshQuery& navQuery, const osg::Vec3f& startPos, - const osg::Vec3f& endPos, const float minTargetDist, const std::vector& path); + const osg::Vec3f& endPos, const float minTargetDist, const dtPolyRef* path, const std::size_t pathSize); template class OutputTransformIterator @@ -125,33 +126,31 @@ namespace DetourNavigator return {std::move(result)}; } - inline std::optional> findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, + inline std::optional findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, - const std::size_t maxSize) + dtPolyRef* path, const std::size_t maxSize) { int pathLen = 0; - std::vector result(maxSize); const auto status = navMeshQuery.findPath(startRef, endRef, startPos.ptr(), endPos.ptr(), &queryFilter, - result.data(), &pathLen, static_cast(maxSize)); + path, &pathLen, static_cast(maxSize)); if (!dtStatusSucceed(status)) return {}; assert(pathLen >= 0); assert(static_cast(pathLen) <= maxSize); - result.resize(static_cast(pathLen)); - return {std::move(result)}; + return static_cast(pathLen); } template Status makeSmoothPath(const dtNavMesh& navMesh, const dtNavMeshQuery& navMeshQuery, const dtQueryFilter& filter, const osg::Vec3f& start, const osg::Vec3f& end, const float stepSize, - std::vector polygonPath, std::size_t maxSmoothPathSize, OutputIterator& out) + std::vector& polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, OutputIterator& out) { // Iterate over the path to find smooth path on the detail mesh surface. osg::Vec3f iterPos; navMeshQuery.closestPointOnPoly(polygonPath.front(), start.ptr(), iterPos.ptr(), nullptr); osg::Vec3f targetPos; - navMeshQuery.closestPointOnPoly(polygonPath.back(), end.ptr(), targetPos.ptr(), nullptr); + navMeshQuery.closestPointOnPoly(polygonPath[polygonPathSize - 1], end.ptr(), targetPos.ptr(), nullptr); constexpr float slop = 0.01f; @@ -161,19 +160,19 @@ namespace DetourNavigator // Move towards target a small advancement at a time until target reached or // when ran out of memory to store the path. - while (!polygonPath.empty() && smoothPathSize < maxSmoothPathSize) + while (polygonPathSize > 0 && smoothPathSize < maxSmoothPathSize) { // Find location to steer towards. - const auto steerTarget = getSteerTarget(navMeshQuery, iterPos, targetPos, slop, polygonPath); + const auto steerTarget = getSteerTarget(navMeshQuery, iterPos, targetPos, slop, polygonPath.data(), polygonPathSize); if (!steerTarget) break; - const bool endOfPath = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_END); - const bool offMeshConnection = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION); + const bool endOfPath = bool(steerTarget->mSteerPosFlag & DT_STRAIGHTPATH_END); + const bool offMeshConnection = bool(steerTarget->mSteerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION); // Find movement delta. - const osg::Vec3f delta = steerTarget->steerPos - iterPos; + const osg::Vec3f delta = steerTarget->mSteerPos - iterPos; float len = delta.length(); // If the steer target is end of path or off-mesh link, do not move past the location. if ((endOfPath || offMeshConnection) && len < stepSize) @@ -187,11 +186,11 @@ namespace DetourNavigator if (!result) return Status::MoveAlongSurfaceFailed; - polygonPath = fixupCorridor(polygonPath, result->mVisited); - polygonPath = fixupShortcuts(polygonPath, navMeshQuery); + polygonPathSize = fixupCorridor(polygonPath, polygonPathSize, result->mVisited); + polygonPathSize = fixupShortcuts(polygonPath.data(), polygonPathSize, navMeshQuery); // Handle end of path and off-mesh links when close enough. - if (endOfPath && inRange(result->mResultPos, steerTarget->steerPos, slop)) + if (endOfPath && inRange(result->mResultPos, steerTarget->mSteerPos, slop)) { // Reached end of path. iterPos = targetPos; @@ -199,20 +198,26 @@ namespace DetourNavigator ++smoothPathSize; break; } - else if (offMeshConnection && inRange(result->mResultPos, steerTarget->steerPos, slop)) + + dtPolyRef polyRef = polygonPath.front(); + osg::Vec3f polyPos = result->mResultPos; + + if (offMeshConnection && inRange(polyPos, steerTarget->mSteerPos, slop)) { // Advance the path up to and over the off-mesh connection. dtPolyRef prevRef = 0; - dtPolyRef polyRef = polygonPath.front(); std::size_t npos = 0; - while (npos < polygonPath.size() && polyRef != steerTarget->steerPosRef) + while (npos < polygonPathSize && polyRef != steerTarget->mSteerPosRef) { prevRef = polyRef; polyRef = polygonPath[npos]; ++npos; } - std::copy(polygonPath.begin() + std::ptrdiff_t(npos), polygonPath.end(), polygonPath.begin()); - polygonPath.resize(polygonPath.size() - npos); + if (npos > 0) + { + std::copy(polygonPath.begin() + npos, polygonPath.begin() + polygonPathSize, polygonPath.begin()); + polygonPathSize -= npos; + } // Reached off-mesh connection. osg::Vec3f startPos; @@ -233,14 +238,11 @@ namespace DetourNavigator } // Move position at the other side of the off-mesh link. - if (dtStatusFailed(navMeshQuery.getPolyHeight(polygonPath.front(), endPos.ptr(), &iterPos.y()))) - return Status::GetPolyHeightFailed; - iterPos.x() = endPos.x(); - iterPos.z() = endPos.z(); + polyPos = endPos; } } - if (dtStatusFailed(navMeshQuery.getPolyHeight(polygonPath.front(), result->mResultPos.ptr(), &iterPos.y()))) + if (dtStatusFailed(navMeshQuery.getPolyHeight(polyRef, polyPos.ptr(), &iterPos.y()))) return Status::GetPolyHeightFailed; iterPos.x() = result->mResultPos.x(); iterPos.z() = result->mResultPos.z(); @@ -281,19 +283,20 @@ namespace DetourNavigator if (endRef == 0) return Status::EndPolygonNotFound; - const auto polygonPath = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter, - settings.mMaxPolygonPathSize); + std::vector polygonPath(settings.mMaxPolygonPathSize); + const auto polygonPathSize = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter, + polygonPath.data(), polygonPath.size()); - if (!polygonPath) + if (!polygonPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (polygonPath->empty()) + if (*polygonPathSize == 0) return Status::Success; - const bool partialPath = polygonPath->back() != endRef; + const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; auto outTransform = OutputTransformIterator(out, settings); const Status smoothStatus = makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize, - std::move(*polygonPath), settings.mMaxSmoothPathSize, outTransform); + polygonPath, *polygonPathSize, settings.mMaxSmoothPathSize, outTransform); if (smoothStatus != Status::Success) return smoothStatus; diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index 27c8f7a4ac..abfce06464 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -46,15 +46,17 @@ namespace DetourNavigator btVector3 aabbMax; shape.getAabb(transform, aabbMin, aabbMax); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(Misc::Convert::toOsg(aabbMin), Misc::Convert::toOsg(aabbMax), settings, std::forward(callback)); } template - void getTilesPositions(const int cellSize, const osg::Vec3f& shift, + void getTilesPositions(const int cellSize, const btVector3& shift, const Settings& settings, Callback&& callback) { + using Misc::Convert::toOsg; + const auto halfCellSize = cellSize / 2; - const btTransform transform(btMatrix3x3::getIdentity(), Misc::Convert::toBullet(shift)); + const btTransform transform(btMatrix3x3::getIdentity(), shift); auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); auto aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0)); @@ -64,7 +66,7 @@ namespace DetourNavigator aabbMax.setX(std::max(aabbMin.x(), aabbMax.x())); aabbMax.setY(std::max(aabbMin.y(), aabbMax.y())); - getTilesPositions(Misc::Convert::makeOsgVec3f(aabbMin), Misc::Convert::makeOsgVec3f(aabbMax), settings, std::forward(callback)); + getTilesPositions(toOsg(aabbMin), toOsg(aabbMax), settings, std::forward(callback)); } } diff --git a/components/detournavigator/heightfieldshape.hpp b/components/detournavigator/heightfieldshape.hpp index 48a273725b..06770e9b3d 100644 --- a/components/detournavigator/heightfieldshape.hpp +++ b/components/detournavigator/heightfieldshape.hpp @@ -1,6 +1,10 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H +#include + +#include + #include #include @@ -20,6 +24,21 @@ namespace DetourNavigator }; using HeightfieldShape = std::variant; + + inline btVector3 getHeightfieldShift(const HeightfieldPlane& v, const osg::Vec2i& cellPosition, int cellSize) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mHeight, v.mHeight); + } + + inline btVector3 getHeightfieldShift(const HeightfieldSurface& v, const osg::Vec2i& cellPosition, int cellSize) + { + return BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, v.mMinHeight, v.mMaxHeight); + } + + inline btVector3 getHeightfieldShift(const HeightfieldShape& v, const osg::Vec2i& cellPosition, int cellSize) + { + return std::visit([&] (const auto& w) { return getHeightfieldShift(w, cellPosition, cellSize); }, v); + } } #endif diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 3f133f5033..7d478b9c1b 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -26,39 +26,38 @@ #include #include +namespace DetourNavigator +{ namespace { - using namespace DetourNavigator; - struct Rectangle { TileBounds mBounds; float mHeight; }; - Rectangle getSwimRectangle(const Cell& water, const Settings& settings, - const osg::Vec3f& agentHalfExtents) + Rectangle getSwimRectangle(const CellWater& water, const Settings& settings, const osg::Vec3f& agentHalfExtents) { - if (water.mSize == std::numeric_limits::max()) + if (water.mWater.mCellSize == std::numeric_limits::max()) { return Rectangle { TileBounds { osg::Vec2f(-std::numeric_limits::max(), -std::numeric_limits::max()), osg::Vec2f(std::numeric_limits::max(), std::numeric_limits::max()) }, - toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z())) + toNavMeshCoordinates(settings, getSwimLevel(settings, water.mWater.mLevel, agentHalfExtents.z())) }; } else { - const osg::Vec2f shift(water.mShift.x(), water.mShift.y()); - const float halfCellSize = water.mSize / 2.0f; + const osg::Vec2f shift = getWaterShift2d(water.mCellPosition, water.mWater.mCellSize); + const float halfCellSize = water.mWater.mCellSize / 2.0f; return Rectangle { TileBounds{ toNavMeshCoordinates(settings, shift + osg::Vec2f(-halfCellSize, -halfCellSize)), toNavMeshCoordinates(settings, shift + osg::Vec2f(halfCellSize, halfCellSize)) }, - toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z())) + toNavMeshCoordinates(settings, getSwimLevel(settings, water.mWater.mLevel, agentHalfExtents.z())) }; } } @@ -121,7 +120,7 @@ namespace return result; } - rcConfig makeConfig(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax, + rcConfig makeConfig(const osg::Vec3f& agentHalfExtents, const TilePosition& tile, float minZ, float maxZ, const Settings& settings) { rcConfig config; @@ -140,15 +139,18 @@ namespace config.detailSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : config.cs * settings.mDetailSampleDist; config.detailSampleMaxError = config.ch * settings.mDetailSampleMaxError; config.borderSize = settings.mBorderSize; - config.width = settings.mTileSize + config.borderSize * 2; - config.height = settings.mTileSize + config.borderSize * 2; - rcVcopy(config.bmin, boundsMin.ptr()); - rcVcopy(config.bmax, boundsMax.ptr()); - config.bmin[0] -= getBorderSize(settings); - config.bmin[2] -= getBorderSize(settings); - config.bmax[0] += getBorderSize(settings); - config.bmax[2] += getBorderSize(settings); config.tileSize = settings.mTileSize; + const int size = config.tileSize + config.borderSize * 2; + config.width = size; + config.height = size; + const float halfBoundsSize = size * config.cs * 0.5f; + const osg::Vec2f shift = osg::Vec2f(tile.x() + 0.5f, tile.y() + 0.5f) * getTileSize(settings); + config.bmin[0] = shift.x() - halfBoundsSize; + config.bmin[1] = minZ; + config.bmin[2] = shift.y() - halfBoundsSize; + config.bmax[0] = shift.x() + halfBoundsSize; + config.bmax[1] = maxZ; + config.bmax[2] = shift.y() + halfBoundsSize; return config; } @@ -198,7 +200,7 @@ namespace } bool rasterizeTriangles(rcContext& context, const Rectangle& rectangle, const rcConfig& config, - const unsigned char* areas, std::size_t areasSize, rcHeightfield& solid) + AreaType areaType, rcHeightfield& solid) { const osg::Vec2f tileBoundsMin( std::clamp(rectangle.mBounds.mMin.x(), config.bmin[0], config.bmax[0]), @@ -224,40 +226,43 @@ namespace 0, 2, 3, }; + const std::array areas {areaType, areaType}; + return rcRasterizeTriangles( &context, vertices.data(), static_cast(vertices.size() / 3), indices.data(), - areas, - static_cast(areasSize), + areas.data(), + static_cast(areas.size()), solid, config.walkableClimb ); } - bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const std::vector& cells, + bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const std::vector& water, const Settings& settings, const rcConfig& config, rcHeightfield& solid) { - const std::array areas {{AreaType_water, AreaType_water}}; - for (const Cell& cell : cells) + for (const CellWater& cellWater : water) { - const Rectangle rectangle = getSwimRectangle(cell, settings, agentHalfExtents); - if (!rasterizeTriangles(context, rectangle, config, areas.data(), areas.size(), solid)) + const Rectangle rectangle = getSwimRectangle(cellWater, settings, agentHalfExtents); + if (!rasterizeTriangles(context, rectangle, config, AreaType_water, solid)) return false; } return true; } - bool rasterizeTriangles(rcContext& context, const std::vector& heightfields, + bool rasterizeTriangles(rcContext& context, const TileBounds& tileBounds, const std::vector& heightfields, const Settings& settings, const rcConfig& config, rcHeightfield& solid) { for (const FlatHeightfield& heightfield : heightfields) { - const std::array areas {{AreaType_ground, AreaType_ground}}; - const Rectangle rectangle {heightfield.mBounds, toNavMeshCoordinates(settings, heightfield.mHeight)}; - if (!rasterizeTriangles(context, rectangle, config, areas.data(), areas.size(), solid)) - return false; + if (auto intersection = getIntersection(tileBounds, maxCellTileBounds(heightfield.mCellPosition, heightfield.mCellSize))) + { + const Rectangle rectangle {*intersection, toNavMeshCoordinates(settings, heightfield.mHeight)}; + if (!rasterizeTriangles(context, rectangle, config, AreaType_ground, solid)) + return false; + } } return true; } @@ -276,13 +281,14 @@ namespace return true; } - bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, - const rcConfig& config, const Settings& settings, rcHeightfield& solid) + bool rasterizeTriangles(rcContext& context, const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, + const RecastMesh& recastMesh, const rcConfig& config, const Settings& settings, rcHeightfield& solid) { return rasterizeTriangles(context, recastMesh.getMesh(), settings, config, solid) && rasterizeTriangles(context, agentHalfExtents, recastMesh.getWater(), settings, config, solid) && rasterizeTriangles(context, recastMesh.getHeightfields(), settings, config, solid) - && rasterizeTriangles(context, recastMesh.getFlatHeightfields(), settings, config, solid); + && rasterizeTriangles(context, makeRealTileBoundsWithBorder(settings, tilePosition), + recastMesh.getFlatHeightfields(), settings, config, solid); } void buildCompactHeightfield(rcContext& context, const int walkableHeight, const int walkableClimb, @@ -357,6 +363,7 @@ namespace rcPolyMeshDetail& polyMeshDetail) { rcCompactHeightfield compact; + compact.dist = nullptr; buildCompactHeightfield(context, config.walkableHeight, config.walkableClimb, solid, compact); erodeWalkableArea(context, config.walkableRadius, compact); @@ -387,24 +394,61 @@ namespace ++power; return power; } + + std::pair getBoundsByZ(const RecastMesh& recastMesh, const osg::Vec3f& agentHalfExtents, const Settings& settings) + { + float minZ = 0; + float maxZ = 0; + + const std::vector& vertices = recastMesh.getMesh().getVertices(); + for (std::size_t i = 0, n = vertices.size(); i < n; i += 3) + { + minZ = std::min(minZ, vertices[i + 2]); + maxZ = std::max(maxZ, vertices[i + 2]); + } + + for (const CellWater& water : recastMesh.getWater()) + { + const float swimLevel = getSwimLevel(settings, water.mWater.mLevel, agentHalfExtents.z()); + minZ = std::min(minZ, swimLevel); + maxZ = std::max(maxZ, swimLevel); + } + + for (const Heightfield& heightfield : recastMesh.getHeightfields()) + { + if (heightfield.mHeights.empty()) + continue; + const auto [minHeight, maxHeight] = std::minmax_element(heightfield.mHeights.begin(), heightfield.mHeights.end()); + minZ = std::min(minZ, *minHeight); + maxZ = std::max(maxZ, *maxHeight); + } + + for (const FlatHeightfield& heightfield : recastMesh.getFlatHeightfields()) + { + minZ = std::min(minZ, heightfield.mHeight); + maxZ = std::max(maxZ, heightfield.mHeight); + } + + return {minZ, maxZ}; + } } +} // namespace DetourNavigator namespace DetourNavigator { std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, - const TilePosition& tile, const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings) + const TilePosition& tilePosition, const osg::Vec3f& agentHalfExtents, const Settings& settings) { - const TileBounds tileBounds = makeTileBounds(settings, tile); - const osg::Vec3f boundsMin(tileBounds.mMin.x(), bounds.mMin.y() - 1, tileBounds.mMin.y()); - const osg::Vec3f boundsMax(tileBounds.mMax.x(), bounds.mMax.y() + 1, tileBounds.mMax.y()); + const auto [minZ, maxZ] = getBoundsByZ(recastMesh, agentHalfExtents, settings); rcContext context; - const auto config = makeConfig(agentHalfExtents, boundsMin, boundsMax, settings); + const auto config = makeConfig(agentHalfExtents, tilePosition, toNavMeshCoordinates(settings, minZ), + toNavMeshCoordinates(settings, maxZ), settings); rcHeightfield solid; createHeightfield(context, solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch); - if (!rasterizeTriangles(context, agentHalfExtents, recastMesh, config, settings, solid)) + if (!rasterizeTriangles(context, tilePosition, agentHalfExtents, recastMesh, config, settings, solid)) return nullptr; rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, solid); @@ -527,18 +571,8 @@ namespace DetourNavigator return navMeshCacheItem->lock()->removeTile(changedTile); } - auto recastMeshBounds = recastMesh->getBounds(); - recastMeshBounds.mMin = toNavMeshCoordinates(settings, recastMeshBounds.mMin); - recastMeshBounds.mMax = toNavMeshCoordinates(settings, recastMeshBounds.mMax); - - for (const auto& water : recastMesh->getWater()) - { - const float height = toNavMeshCoordinates(settings, getSwimLevel(settings, water.mShift.z(), agentHalfExtents.z())); - recastMeshBounds.mMin.y() = std::min(recastMeshBounds.mMin.y(), height); - recastMeshBounds.mMax.y() = std::max(recastMeshBounds.mMax.y(), height); - } - - if (isEmpty(recastMeshBounds)) + if (recastMesh->getMesh().getIndices().empty() && recastMesh->getWater().empty() + && recastMesh->getHeightfields().empty() && recastMesh->getFlatHeightfields().empty()) { Log(Debug::Debug) << "Ignore add tile: recastMesh is empty"; return navMeshCacheItem->lock()->removeTile(changedTile); @@ -557,8 +591,7 @@ namespace DetourNavigator if (!cachedNavMeshData) { - auto prepared = prepareNavMeshTileData(*recastMesh, changedTile, recastMeshBounds, - agentHalfExtents, settings); + auto prepared = prepareNavMeshTileData(*recastMesh, changedTile, agentHalfExtents, settings); if (prepared == nullptr) { diff --git a/components/detournavigator/navigator.cpp b/components/detournavigator/navigator.cpp index 700217c52f..1877c3dd93 100644 --- a/components/detournavigator/navigator.cpp +++ b/components/detournavigator/navigator.cpp @@ -1,36 +1,18 @@ -#include "findrandompointaroundcircle.hpp" #include "navigator.hpp" -#include "raycast.hpp" +#include "navigatorimpl.hpp" +#include "navigatorstub.hpp" +#include "recastglobalallocator.hpp" namespace DetourNavigator { - std::optional Navigator::findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const + std::unique_ptr makeNavigator(const Settings& settings) { - const auto navMesh = getNavMesh(agentHalfExtents); - if (!navMesh) - return std::optional(); - const auto settings = getSettings(); - const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(), - toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, maxRadius), includeFlags, settings); - if (!result) - return std::optional(); - return std::optional(fromNavMeshCoordinates(settings, *result)); + DetourNavigator::RecastGlobalAllocator::init(); + return std::make_unique(settings); } - std::optional Navigator::raycast(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start, - const osg::Vec3f& end, const Flags includeFlags) const + std::unique_ptr makeNavigatorStub() { - const auto navMesh = getNavMesh(agentHalfExtents); - if (navMesh == nullptr) - return {}; - const auto settings = getSettings(); - const auto result = DetourNavigator::raycast(navMesh->lockConst()->getImpl(), - toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, end), includeFlags, settings); - if (!result) - return {}; - return fromNavMeshCoordinates(settings, *result); + return std::make_unique(); } } diff --git a/components/detournavigator/navigator.hpp b/components/detournavigator/navigator.hpp index d2e77892b9..6070a19fa8 100644 --- a/components/detournavigator/navigator.hpp +++ b/components/detournavigator/navigator.hpp @@ -1,9 +1,6 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H -#include "findsmoothpath.hpp" -#include "flags.hpp" -#include "settings.hpp" #include "objectid.hpp" #include "navmeshcacheitem.hpp" #include "recastmeshtiles.hpp" @@ -12,8 +9,6 @@ #include -#include - namespace ESM { struct Cell; @@ -27,6 +22,8 @@ namespace Loading namespace DetourNavigator { + struct Settings; + struct ObjectShapes { osg::ref_ptr mShapeInstance; @@ -125,7 +122,7 @@ namespace DetourNavigator * at least single object is added to the scene, false if there is already water for given cell or there is no * any other objects. */ - virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) = 0; + virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) = 0; /** * @brief removeWater to make it no more available at the scene. @@ -134,8 +131,7 @@ namespace DetourNavigator */ virtual bool removeWater(const osg::Vec2i& cellPosition) = 0; - virtual bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape) = 0; + virtual bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) = 0; virtual bool removeHeightfield(const osg::Vec2i& cellPosition) = 0; @@ -166,38 +162,6 @@ namespace DetourNavigator */ virtual void wait(Loading::Listener& listener, WaitConditionType waitConditionType) = 0; - /** - * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. - * @param agentHalfExtents allows to find navmesh for given actor. - * @param start path from given point. - * @param end path at given point. - * @param includeFlags setup allowed surfaces for actor to walk. - * @param out the beginning of the destination range. - * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents - * @return Output iterator to the element in the destination range, one past the last element of found path. - * Equal to out if no path is found. - */ - template - Status findPath(const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start, - const osg::Vec3f& end, const Flags includeFlags, const DetourNavigator::AreaCosts& areaCosts, - float endTolerance, OutputIterator& out) const - { - static_assert( - std::is_same< - typename std::iterator_traits::iterator_category, - std::output_iterator_tag - >::value, - "out is not an OutputIterator" - ); - const auto navMesh = getNavMesh(agentHalfExtents); - if (!navMesh) - return Status::NavMeshNotFound; - const auto settings = getSettings(); - return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings, agentHalfExtents), - toNavMeshCoordinates(settings, stepSize), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, endTolerance, out); - } - /** * @brief getNavMesh returns navmesh for specific agent half extents * @return navmesh @@ -214,32 +178,14 @@ namespace DetourNavigator virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; - /** - * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. - * @param agentHalfExtents allows to find navmesh for given actor. - * @param start path from given point. - * @param maxRadius limit maximum distance from start. - * @param includeFlags setup allowed surfaces for actor to walk. - * @return not empty optional with position if point is found and empty optional if point is not found. - */ - std::optional findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents, - const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const; - - /** - * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start. - * @param agentHalfExtents allows to find navmesh for given actor. - * @param start of the line - * @param end of the line - * @param includeFlags setup allowed surfaces for actor to walk. - * @return not empty optional with position if point is found and empty optional if point is not found. - */ - std::optional raycast(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start, - const osg::Vec3f& end, const Flags includeFlags) const; - - virtual RecastMeshTiles getRecastMeshTiles() = 0; + virtual RecastMeshTiles getRecastMeshTiles() const = 0; virtual float getMaxNavmeshAreaRealRadius() const = 0; }; + + std::unique_ptr makeNavigator(const Settings& settings); + + std::unique_ptr makeNavigatorStub(); } #endif diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index 750901c73e..f209a24c2c 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -34,9 +34,9 @@ namespace DetourNavigator bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; @@ -64,13 +64,13 @@ namespace DetourNavigator bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->mCollisionShape}; bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground); - if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->mAvoidCollisionShape.get()) { const ObjectId avoidId(avoidShape); - const CollisionShape collisionShape {shapes.mShapeInstance, *avoidShape}; - if (mNavMeshManager.updateObject(avoidId, collisionShape, transform, AreaType_null)) + const CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.updateObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; @@ -97,9 +97,9 @@ namespace DetourNavigator return result; } - bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) + bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - return mNavMeshManager.addWater(cellPosition, cellSize, shift); + return mNavMeshManager.addWater(cellPosition, cellSize, level); } bool NavigatorImpl::removeWater(const osg::Vec2i& cellPosition) @@ -107,10 +107,9 @@ namespace DetourNavigator return mNavMeshManager.removeWater(cellPosition); } - bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape) + bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) { - return mNavMeshManager.addHeightfield(cellPosition, cellSize, shift, shape); + return mNavMeshManager.addHeightfield(cellPosition, cellSize, shape); } bool NavigatorImpl::removeHeightfield(const osg::Vec2i& cellPosition) @@ -187,7 +186,7 @@ namespace DetourNavigator mNavMeshManager.reportStats(frameNumber, stats); } - RecastMeshTiles NavigatorImpl::getRecastMeshTiles() + RecastMeshTiles NavigatorImpl::getRecastMeshTiles() const { return mNavMeshManager.getRecastMeshTiles(); } diff --git a/components/detournavigator/navigatorimpl.hpp b/components/detournavigator/navigatorimpl.hpp index cda6158958..2c8474786f 100644 --- a/components/detournavigator/navigatorimpl.hpp +++ b/components/detournavigator/navigatorimpl.hpp @@ -31,12 +31,11 @@ namespace DetourNavigator bool removeObject(const ObjectId id) override; - bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) override; + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level) override; bool removeWater(const osg::Vec2i& cellPosition) override; - bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape) override; + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) override; bool removeHeightfield(const osg::Vec2i& cellPosition) override; @@ -60,7 +59,7 @@ namespace DetourNavigator void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; - RecastMeshTiles getRecastMeshTiles() override; + RecastMeshTiles getRecastMeshTiles() const override; float getMaxNavmeshAreaRealRadius() const override; diff --git a/components/detournavigator/navigatorstub.hpp b/components/detournavigator/navigatorstub.hpp index 40de90f256..1a6096feef 100644 --- a/components/detournavigator/navigatorstub.hpp +++ b/components/detournavigator/navigatorstub.hpp @@ -44,7 +44,7 @@ namespace DetourNavigator return false; } - bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/) override + bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, float /*level*/) override { return false; } @@ -54,8 +54,7 @@ namespace DetourNavigator return false; } - bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/, - const HeightfieldShape& /*height*/) override + bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const HeightfieldShape& /*height*/) override { return false; } @@ -94,7 +93,7 @@ namespace DetourNavigator void reportStats(unsigned int /*frameNumber*/, osg::Stats& /*stats*/) const override {} - RecastMeshTiles getRecastMeshTiles() override + RecastMeshTiles getRecastMeshTiles() const override { return {}; } diff --git a/components/detournavigator/navigatorutils.cpp b/components/detournavigator/navigatorutils.cpp new file mode 100644 index 0000000000..82a108db6f --- /dev/null +++ b/components/detournavigator/navigatorutils.cpp @@ -0,0 +1,37 @@ +#include "navigatorutils.hpp" +#include "findrandompointaroundcircle.hpp" +#include "navigator.hpp" +#include "raycast.hpp" + +namespace DetourNavigator +{ + std::optional findRandomPointAroundCircle(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) + { + const auto navMesh = navigator.getNavMesh(agentHalfExtents); + if (!navMesh) + return std::nullopt; + const auto settings = navigator.getSettings(); + const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(), + toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start), + toNavMeshCoordinates(settings, maxRadius), includeFlags, settings); + if (!result) + return std::nullopt; + return std::optional(fromNavMeshCoordinates(settings, *result)); + } + + std::optional raycast(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start, + const osg::Vec3f& end, const Flags includeFlags) + { + const auto navMesh = navigator.getNavMesh(agentHalfExtents); + if (navMesh == nullptr) + return std::nullopt; + const auto settings = navigator.getSettings(); + const auto result = DetourNavigator::raycast(navMesh->lockConst()->getImpl(), + toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start), + toNavMeshCoordinates(settings, end), includeFlags, settings); + if (!result) + return std::nullopt; + return fromNavMeshCoordinates(settings, *result); + } +} diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp new file mode 100644 index 0000000000..4ccc238f97 --- /dev/null +++ b/components/detournavigator/navigatorutils.hpp @@ -0,0 +1,68 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATORUTILS_H + +#include "findsmoothpath.hpp" +#include "flags.hpp" +#include "settings.hpp" +#include "navigator.hpp" + +#include + +namespace DetourNavigator +{ + /** + * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. + * @param agentHalfExtents allows to find navmesh for given actor. + * @param start path from given point. + * @param end path at given point. + * @param includeFlags setup allowed surfaces for actor to walk. + * @param out the beginning of the destination range. + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents + * @return Output iterator to the element in the destination range, one past the last element of found path. + * Equal to out if no path is found. + */ + template + inline Status findPath(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start, + const osg::Vec3f& end, const Flags includeFlags, const DetourNavigator::AreaCosts& areaCosts, + float endTolerance, OutputIterator& out) + { + static_assert( + std::is_same< + typename std::iterator_traits::iterator_category, + std::output_iterator_tag + >::value, + "out is not an OutputIterator" + ); + const auto navMesh = navigator.getNavMesh(agentHalfExtents); + if (navMesh == nullptr) + return Status::NavMeshNotFound; + const auto settings = navigator.getSettings(); + return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings, agentHalfExtents), + toNavMeshCoordinates(settings, stepSize), toNavMeshCoordinates(settings, start), + toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, endTolerance, out); + } + + /** + * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. + * @param agentHalfExtents allows to find navmesh for given actor. + * @param start path from given point. + * @param maxRadius limit maximum distance from start. + * @param includeFlags setup allowed surfaces for actor to walk. + * @return not empty optional with position if point is found and empty optional if point is not found. + */ + std::optional findRandomPointAroundCircle(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, + const osg::Vec3f& start, const float maxRadius, const Flags includeFlags); + + /** + * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start. + * @param agentHalfExtents allows to find navmesh for given actor. + * @param start of the line + * @param end of the line + * @param includeFlags setup allowed surfaces for actor to walk. + * @return not empty optional with position if point is found and empty optional if point is not found. + */ + std::optional raycast(const Navigator& navigator, const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start, + const osg::Vec3f& end, const Flags includeFlags); +} + +#endif diff --git a/components/detournavigator/navmeshcacheitem.cpp b/components/detournavigator/navmeshcacheitem.cpp index ee6f3308d0..decf45de2c 100644 --- a/components/detournavigator/navmeshcacheitem.cpp +++ b/components/detournavigator/navmeshcacheitem.cpp @@ -13,12 +13,6 @@ namespace { using DetourNavigator::TilePosition; - const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position) - { - const int layer = 0; - return navMesh.getTileAt(position.x(), position.y(), layer); - } - bool removeTile(dtNavMesh& navMesh, const TilePosition& position) { const int layer = 0; @@ -41,10 +35,16 @@ namespace namespace DetourNavigator { + const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position) + { + const int layer = 0; + return navMesh.getTileAt(position.x(), position.y(), layer); + } + UpdateNavMeshStatus NavMeshCacheItem::updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, NavMeshData&& navMeshData) { - const dtMeshTile* currentTile = ::getTile(*mImpl, position); + const dtMeshTile* currentTile = getTile(*mImpl, position); if (currentTile != nullptr && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(navMeshData.mValue.get())) { @@ -54,8 +54,19 @@ namespace DetourNavigator const auto addStatus = addTile(*mImpl, navMeshData.mValue.get(), navMeshData.mSize); if (dtStatusSucceed(addStatus)) { - mUsedTiles[position] = std::make_pair(std::move(cached), std::move(navMeshData)); - ++mNavMeshRevision; + auto tile = mUsedTiles.find(position); + if (tile == mUsedTiles.end()) + { + mUsedTiles.emplace_hint(tile, position, + Tile {Version {mVersion.mRevision, 1}, std::move(cached), std::move(navMeshData)}); + } + else + { + ++tile->second.mVersion.mRevision; + tile->second.mCached = std::move(cached); + tile->second.mData = std::move(navMeshData); + } + ++mVersion.mRevision; return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult(); } else @@ -63,7 +74,7 @@ namespace DetourNavigator if (removed) { mUsedTiles.erase(position); - ++mNavMeshRevision; + ++mVersion.mRevision; } return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult(); } @@ -75,7 +86,7 @@ namespace DetourNavigator if (removed) { mUsedTiles.erase(position); - ++mNavMeshRevision; + ++mVersion.mRevision; } return UpdateNavMeshStatusBuilder().removed(removed).getResult(); } diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index ac68caedb3..5d3b404080 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -6,6 +6,7 @@ #include "navmeshtilescache.hpp" #include "dtstatus.hpp" #include "navmeshdata.hpp" +#include "version.hpp" #include @@ -123,11 +124,14 @@ namespace DetourNavigator } }; + const dtMeshTile* getTile(const dtNavMesh& navMesh, const TilePosition& position); + class NavMeshCacheItem { public: NavMeshCacheItem(const NavMeshPtr& impl, std::size_t generation) - : mImpl(impl), mGeneration(generation), mNavMeshRevision(0) + : mImpl(impl) + , mVersion {generation, 0} { } @@ -136,26 +140,32 @@ namespace DetourNavigator return *mImpl; } - std::size_t getGeneration() const - { - return mGeneration; - } - - std::size_t getNavMeshRevision() const - { - return mNavMeshRevision; - } + const Version& getVersion() const { return mVersion; } UpdateNavMeshStatus updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, NavMeshData&& navMeshData); UpdateNavMeshStatus removeTile(const TilePosition& position); + template + void forEachUsedTile(Function&& function) const + { + for (const auto& [position, tile] : mUsedTiles) + if (const dtMeshTile* meshTile = getTile(*mImpl, position)) + function(position, tile.mVersion, *meshTile); + } + private: + struct Tile + { + Version mVersion; + NavMeshTilesCache::Value mCached; + NavMeshData mData; + }; + NavMeshPtr mImpl; - std::size_t mGeneration; - std::size_t mNavMeshRevision; - std::map> mUsedTiles; + Version mVersion; + std::map mUsedTiles; }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index c378230845..fff7d8d7cc 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -8,6 +8,7 @@ #include "waitconditiontype.hpp" #include +#include #include @@ -73,10 +74,11 @@ namespace DetourNavigator return true; } - bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift) + bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - if (!mRecastMeshManager.addWater(cellPosition, cellSize, shift)) + if (!mRecastMeshManager.addWater(cellPosition, cellSize, level)) return false; + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level)); addChangedTiles(cellSize, shift, ChangeType::add); return true; } @@ -86,15 +88,16 @@ namespace DetourNavigator const auto water = mRecastMeshManager.removeWater(cellPosition); if (!water) return false; - addChangedTiles(water->mSize, water->mShift, ChangeType::remove); + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, water->mCellSize, water->mLevel)); + addChangedTiles(water->mCellSize, shift, ChangeType::remove); return true; } - bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape) + bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) { - if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shift, shape)) + if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shape)) return false; + const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize); addChangedTiles(cellSize, shift, ChangeType::add); return true; } @@ -104,7 +107,8 @@ namespace DetourNavigator const auto heightfield = mRecastMeshManager.removeHeightfield(cellPosition); if (!heightfield) return false; - addChangedTiles(heightfield->mSize, heightfield->mShift, ChangeType::remove); + const btVector3 shift = getHeightfieldShift(heightfield->mShape, cellPosition, heightfield->mCellSize); + addChangedTiles(heightfield->mCellSize, shift, ChangeType::remove); return true; } @@ -232,14 +236,15 @@ namespace DetourNavigator mAsyncNavMeshUpdater.reportStats(frameNumber, stats); } - RecastMeshTiles NavMeshManager::getRecastMeshTiles() + RecastMeshTiles NavMeshManager::getRecastMeshTiles() const { std::vector tiles; mRecastMeshManager.forEachTile( [&tiles] (const TilePosition& tile, const CachedRecastMeshManager&) { tiles.push_back(tile); }); RecastMeshTiles result; - std::transform(tiles.begin(), tiles.end(), std::inserter(result, result.end()), - [this] (const TilePosition& tile) { return std::make_pair(tile, mRecastMeshManager.getMesh(tile)); }); + for (const TilePosition& tile : tiles) + if (auto mesh = mRecastMeshManager.getCachedMesh(tile)) + result.emplace(tile, std::move(mesh)); return result; } @@ -250,7 +255,7 @@ namespace DetourNavigator [&] (const TilePosition& v) { addChangedTile(v, changeType); }); } - void NavMeshManager::addChangedTiles(const int cellSize, const osg::Vec3f& shift, + void NavMeshManager::addChangedTiles(const int cellSize, const btVector3& shift, const ChangeType changeType) { if (cellSize == std::numeric_limits::max()) diff --git a/components/detournavigator/navmeshmanager.hpp b/components/detournavigator/navmeshmanager.hpp index 76c9b1e0b9..b1926741c5 100644 --- a/components/detournavigator/navmeshmanager.hpp +++ b/components/detournavigator/navmeshmanager.hpp @@ -34,12 +34,11 @@ namespace DetourNavigator void addAgent(const osg::Vec3f& agentHalfExtents); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); bool removeWater(const osg::Vec2i& cellPosition); - bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); bool removeHeightfield(const osg::Vec2i& cellPosition); @@ -59,7 +58,7 @@ namespace DetourNavigator void reportStats(unsigned int frameNumber, osg::Stats& stats) const; - RecastMeshTiles getRecastMeshTiles(); + RecastMeshTiles getRecastMeshTiles() const; private: const Settings& mSettings; @@ -74,7 +73,7 @@ namespace DetourNavigator void addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType); - void addChangedTiles(const int cellSize, const osg::Vec3f& shift, const ChangeType changeType); + void addChangedTiles(const int cellSize, const btVector3& shift, const ChangeType changeType); void addChangedTile(const TilePosition& tilePosition, const ChangeType changeType); diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index 06d6c69e9b..fdafa0c6d6 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -23,7 +23,7 @@ namespace DetourNavigator struct RecastMeshData { Mesh mMesh; - std::vector mWater; + std::vector mWater; std::vector mHeightfields; std::vector mFlatHeightfields; }; diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp index 336cd1ba84..d12bcecd7e 100644 --- a/components/detournavigator/navmeshtileview.cpp +++ b/components/detournavigator/navmeshtileview.cpp @@ -9,14 +9,11 @@ #include #include -namespace +inline bool operator==(const dtMeshHeader& lhs, const dtMeshHeader& rhs) noexcept { - using DetourNavigator::ArrayRef; - using DetourNavigator::Ref; - using DetourNavigator::Span; - - auto makeTuple(const dtMeshHeader& v) + const auto makeTuple = [] (const dtMeshHeader& v) { + using DetourNavigator::ArrayRef; return std::tuple( v.x, v.y, @@ -39,47 +36,46 @@ namespace ArrayRef(v.bmax), v.bvQuantFactor ); - } + }; + return makeTuple(lhs) == makeTuple(rhs); +} - auto makeTuple(const dtPoly& v) +inline bool operator==(const dtPoly& lhs, const dtPoly& rhs) noexcept +{ + const auto makeTuple = [] (const dtPoly& v) { + using DetourNavigator::ArrayRef; return std::tuple(ArrayRef(v.verts), ArrayRef(v.neis), v.flags, v.vertCount, v.areaAndtype); - } + }; + return makeTuple(lhs) == makeTuple(rhs); +} - auto makeTuple(const dtPolyDetail& v) +inline bool operator==(const dtPolyDetail& lhs, const dtPolyDetail& rhs) noexcept +{ + const auto makeTuple = [] (const dtPolyDetail& v) { return std::tuple(v.vertBase, v.triBase, v.vertCount, v.triCount); - } + }; + return makeTuple(lhs) == makeTuple(rhs); +} - auto makeTuple(const dtBVNode& v) +inline bool operator==(const dtBVNode& lhs, const dtBVNode& rhs) noexcept +{ + const auto makeTuple = [] (const dtBVNode& v) { + using DetourNavigator::ArrayRef; return std::tuple(ArrayRef(v.bmin), ArrayRef(v.bmax), v.i); - } - - 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) - ); - } + }; + return makeTuple(lhs) == makeTuple(rhs); } -template -inline auto operator==(const T& lhs, const T& rhs) - -> std::enable_if_t, void>, bool> +inline bool operator==(const dtOffMeshConnection& lhs, const dtOffMeshConnection& rhs) noexcept { + const auto makeTuple = [] (const dtOffMeshConnection& v) + { + using DetourNavigator::ArrayRef; + return std::tuple(ArrayRef(v.pos), v.rad, v.poly, v.flags, v.side, v.userId); + }; return makeTuple(lhs) == makeTuple(rhs); } @@ -139,8 +135,23 @@ namespace DetourNavigator return view; } - bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) + bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept { + using DetourNavigator::Ref; + using DetourNavigator::Span; + const auto makeTuple = [] (const DetourNavigator::NavMeshTileConstView& v) + { + return std::tuple( + Ref(*v.mHeader), + Span(v.mPolys, v.mHeader->polyCount), + Span(v.mVerts, v.mHeader->vertCount), + Span(v.mDetailMeshes, v.mHeader->detailMeshCount), + Span(v.mDetailVerts, v.mHeader->detailVertCount), + Span(v.mDetailTris, v.mHeader->detailTriCount), + Span(v.mBvTree, v.mHeader->bvNodeCount), + Span(v.mOffMeshCons, v.mHeader->offMeshConCount) + ); + }; return makeTuple(lhs) == makeTuple(rhs); } } diff --git a/components/detournavigator/navmeshtileview.hpp b/components/detournavigator/navmeshtileview.hpp index 92017360c3..b797545b8a 100644 --- a/components/detournavigator/navmeshtileview.hpp +++ b/components/detournavigator/navmeshtileview.hpp @@ -21,7 +21,7 @@ namespace DetourNavigator const dtBVNode* mBvTree; const dtOffMeshConnection* mOffMeshCons; - friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs); + friend bool operator==(const NavMeshTileConstView& lhs, const NavMeshTileConstView& rhs) noexcept; }; NavMeshTileConstView asNavMeshTileConstView(const unsigned char* data); diff --git a/components/detournavigator/preparednavmeshdata.cpp b/components/detournavigator/preparednavmeshdata.cpp index 3fea46b26c..c86e18482c 100644 --- a/components/detournavigator/preparednavmeshdata.cpp +++ b/components/detournavigator/preparednavmeshdata.cpp @@ -6,8 +6,6 @@ namespace { - using namespace DetourNavigator; - void initPolyMeshDetail(rcPolyMeshDetail& value) noexcept { value.meshes = nullptr; @@ -26,13 +24,6 @@ namespace } } -template -inline constexpr auto operator==(const T& lhs, const T& rhs) noexcept - -> std::enable_if_t, void>, bool> -{ - return makeTuple(lhs) == makeTuple(rhs); -} - namespace DetourNavigator { PreparedNavMeshData::PreparedNavMeshData() noexcept diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp index 8ff1267370..03b192ad38 100644 --- a/components/detournavigator/preparednavmeshdatatuple.hpp +++ b/components/detournavigator/preparednavmeshdatatuple.hpp @@ -9,16 +9,17 @@ #include -namespace DetourNavigator +inline bool operator==(const rcPolyMesh& lhs, const rcPolyMesh& rhs) noexcept { - constexpr auto makeTuple(const rcPolyMesh& v) noexcept + const auto makeTuple = [] (const rcPolyMesh& v) { + using namespace DetourNavigator; return std::tuple( - Span(v.verts, getVertsLength(v)), - Span(v.polys, getPolysLength(v)), - Span(v.regs, getRegsLength(v)), - Span(v.flags, getFlagsLength(v)), - Span(v.areas, getAreasLength(v)), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.polys, static_cast(getPolysLength(v))), + Span(v.regs, static_cast(getRegsLength(v))), + Span(v.flags, static_cast(getFlagsLength(v))), + Span(v.areas, static_cast(getAreasLength(v))), ArrayRef(v.bmin), ArrayRef(v.bmax), v.cs, @@ -26,18 +27,27 @@ namespace DetourNavigator v.borderSize, v.maxEdgeError ); - } + }; + return makeTuple(lhs) == makeTuple(rhs); +} - constexpr auto makeTuple(const rcPolyMeshDetail& v) noexcept +inline bool operator==(const rcPolyMeshDetail& lhs, const rcPolyMeshDetail& rhs) noexcept +{ + const auto makeTuple = [] (const rcPolyMeshDetail& v) { + using namespace DetourNavigator; return std::tuple( - Span(v.meshes, getMeshesLength(v)), - Span(v.verts, getVertsLength(v)), - Span(v.tris, getTrisLength(v)) + Span(v.meshes, static_cast(getMeshesLength(v))), + Span(v.verts, static_cast(getVertsLength(v))), + Span(v.tris, static_cast(getTrisLength(v))) ); - } + }; + return makeTuple(lhs) == makeTuple(rhs); +} - constexpr auto makeTuple(const PreparedNavMeshData& v) noexcept +namespace DetourNavigator +{ + inline auto makeTuple(const PreparedNavMeshData& v) noexcept { return std::tuple( v.mUserId, diff --git a/components/detournavigator/recastmesh.cpp b/components/detournavigator/recastmesh.cpp index e2dea0ad6e..93a0e171a1 100644 --- a/components/detournavigator/recastmesh.cpp +++ b/components/detournavigator/recastmesh.cpp @@ -18,7 +18,7 @@ namespace DetourNavigator mAreaTypes = std::move(areaTypes); } - RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, + RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, std::vector heightfields, std::vector flatHeightfields) : mGeneration(generation) , mRevision(revision) @@ -27,31 +27,9 @@ namespace DetourNavigator , mHeightfields(std::move(heightfields)) , mFlatHeightfields(std::move(flatHeightfields)) { - if (mMesh.getVerticesCount() > 0) - rcCalcBounds(mMesh.getVertices().data(), static_cast(mMesh.getVerticesCount()), - mBounds.mMin.ptr(), mBounds.mMax.ptr()); mWater.shrink_to_fit(); mHeightfields.shrink_to_fit(); for (Heightfield& v : mHeightfields) v.mHeights.shrink_to_fit(); - for (const Heightfield& v : mHeightfields) - { - const auto [min, max] = std::minmax_element(v.mHeights.begin(), v.mHeights.end()); - mBounds.mMin.x() = std::min(mBounds.mMin.x(), v.mBounds.mMin.x()); - mBounds.mMin.y() = std::min(mBounds.mMin.y(), v.mBounds.mMin.y()); - mBounds.mMin.z() = std::min(mBounds.mMin.z(), *min); - mBounds.mMax.x() = std::max(mBounds.mMax.x(), v.mBounds.mMax.x()); - mBounds.mMax.y() = std::max(mBounds.mMax.y(), v.mBounds.mMax.y()); - mBounds.mMax.z() = std::max(mBounds.mMax.z(), *max); - } - for (const FlatHeightfield& v : mFlatHeightfields) - { - mBounds.mMin.x() = std::min(mBounds.mMin.x(), v.mBounds.mMin.x()); - mBounds.mMin.y() = std::min(mBounds.mMin.y(), v.mBounds.mMin.y()); - mBounds.mMin.z() = std::min(mBounds.mMin.z(), v.mHeight); - mBounds.mMax.x() = std::max(mBounds.mMax.x(), v.mBounds.mMax.x()); - mBounds.mMax.y() = std::max(mBounds.mMax.y(), v.mBounds.mMax.y()); - mBounds.mMax.z() = std::max(mBounds.mMax.z(), v.mHeight); - } } } diff --git a/components/detournavigator/recastmesh.hpp b/components/detournavigator/recastmesh.hpp index c8e160603b..3cfe9e1cab 100644 --- a/components/detournavigator/recastmesh.hpp +++ b/components/detournavigator/recastmesh.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -47,26 +48,57 @@ namespace DetourNavigator } }; - struct Cell + struct Water { - int mSize; - osg::Vec3f mShift; + int mCellSize; + float mLevel; }; + inline bool operator<(const Water& lhs, const Water& rhs) noexcept + { + const auto tie = [] (const Water& v) { return std::tie(v.mCellSize, v.mLevel); }; + return tie(lhs) < tie(rhs); + } + + struct CellWater + { + osg::Vec2i mCellPosition; + Water mWater; + }; + + inline bool operator<(const CellWater& lhs, const CellWater& rhs) noexcept + { + const auto tie = [] (const CellWater& v) { return std::tie(v.mCellPosition, v.mWater); }; + return tie(lhs) < tie(rhs); + } + + inline osg::Vec2f getWaterShift2d(const osg::Vec2i& cellPosition, int cellSize) + { + return osg::Vec2f((cellPosition.x() + 0.5f) * cellSize, (cellPosition.y() + 0.5f) * cellSize); + } + + inline osg::Vec3f getWaterShift3d(const osg::Vec2i& cellPosition, int cellSize, float level) + { + return osg::Vec3f(getWaterShift2d(cellPosition, cellSize), level); + } + struct Heightfield { - TileBounds mBounds; + osg::Vec2i mCellPosition; + int mCellSize; std::uint8_t mLength; float mMinHeight; float mMaxHeight; - osg::Vec3f mShift; - float mScale; std::vector mHeights; + std::size_t mOriginalSize; + std::uint8_t mMinX; + std::uint8_t mMinY; }; inline auto makeTuple(const Heightfield& v) noexcept { - return std::tie(v.mBounds, v.mLength, v.mMinHeight, v.mMaxHeight, v.mShift, v.mScale, v.mHeights); + return std::tie(v.mCellPosition, v.mCellSize, v.mLength, v.mMinHeight, v.mMaxHeight, + v.mHeights, v.mOriginalSize, v.mMinX, v.mMinY); } inline bool operator<(const Heightfield& lhs, const Heightfield& rhs) noexcept @@ -76,19 +108,21 @@ namespace DetourNavigator struct FlatHeightfield { - TileBounds mBounds; + osg::Vec2i mCellPosition; + int mCellSize; float mHeight; }; inline bool operator<(const FlatHeightfield& lhs, const FlatHeightfield& rhs) noexcept { - return std::tie(lhs.mBounds, lhs.mHeight) < std::tie(rhs.mBounds, rhs.mHeight); + const auto tie = [] (const FlatHeightfield& v) { return std::tie(v.mCellPosition, v.mCellSize, v.mHeight); }; + return tie(lhs) < tie(rhs); } class RecastMesh { public: - RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, + RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, std::vector heightfields, std::vector flatHeightfields); std::size_t getGeneration() const @@ -103,7 +137,7 @@ namespace DetourNavigator const Mesh& getMesh() const noexcept { return mMesh; } - const std::vector& getWater() const + const std::vector& getWater() const { return mWater; } @@ -118,39 +152,23 @@ namespace DetourNavigator return mFlatHeightfields; } - const Bounds& getBounds() const - { - return mBounds; - } - private: std::size_t mGeneration; std::size_t mRevision; Mesh mMesh; - std::vector mWater; + std::vector mWater; std::vector mHeightfields; std::vector mFlatHeightfields; - Bounds mBounds; - - friend inline bool operator <(const RecastMesh& lhs, const RecastMesh& rhs) noexcept - { - return std::tie(lhs.mMesh, lhs.mWater) < std::tie(rhs.mMesh, rhs.mWater); - } friend inline std::size_t getSize(const RecastMesh& value) noexcept { - return getSize(value.mMesh) + value.mWater.size() * sizeof(Cell) + return getSize(value.mMesh) + value.mWater.size() * sizeof(CellWater) + value.mHeightfields.size() * sizeof(Heightfield) + std::accumulate(value.mHeightfields.begin(), value.mHeightfields.end(), std::size_t {0}, [] (std::size_t r, const Heightfield& v) { return r + v.mHeights.size() * sizeof(float); }) + value.mFlatHeightfields.size() * sizeof(FlatHeightfield); } }; - - inline bool operator<(const Cell& lhs, const Cell& rhs) noexcept - { - return std::tie(lhs.mSize, lhs.mShift) < std::tie(rhs.mSize, rhs.mShift); - } } #endif diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 73b731c247..b54b927696 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include namespace DetourNavigator { @@ -30,17 +32,13 @@ namespace DetourNavigator RecastMeshTriangle result; result.mAreaType = areaType; for (std::size_t i = 0; i < 3; ++i) - result.mVertices[i] = Misc::Convert::makeOsgVec3f(vertices[i]); + result.mVertices[i] = Misc::Convert::toOsg(vertices[i]); return result; } - TileBounds maxCellTileBounds(int size, const osg::Vec3f& shift) + float getHeightfieldScale(int cellSize, std::size_t dataSize) { - const float halfCellSize = static_cast(size) / 2; - return TileBounds { - osg::Vec2f(shift.x() - halfCellSize, shift.y() - halfCellSize), - osg::Vec2f(shift.x() + halfCellSize, shift.y() + halfCellSize) - }; + return static_cast(cellSize) / (dataSize - 1); } } @@ -107,7 +105,8 @@ namespace DetourNavigator static_cast(heightfield.mLength), heightfield.mHeights.data(), heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, flipQuadEdges); #endif - shape.setLocalScaling(btVector3(heightfield.mScale, heightfield.mScale, 1)); + const float scale = getHeightfieldScale(heightfield.mCellSize, heightfield.mOriginalSize); + shape.setLocalScaling(btVector3(scale, scale, 1)); btVector3 aabbMin; btVector3 aabbMax; shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax); @@ -117,8 +116,16 @@ namespace DetourNavigator triangles.emplace_back(makeRecastMeshTriangle(vertices, AreaType_ground)); }); shape.processAllTriangles(&callback, aabbMin, aabbMax); - const osg::Vec2f shift = (osg::Vec2f(aabbMax.x(), aabbMax.y()) - osg::Vec2f(aabbMin.x(), aabbMin.y())) * 0.5; - return makeMesh(std::move(triangles), heightfield.mShift + osg::Vec3f(shift.x(), shift.y(), 0)); + const osg::Vec2f aabbShift = (osg::Vec2f(aabbMax.x(), aabbMax.y()) - osg::Vec2f(aabbMin.x(), aabbMin.y())) * 0.5; + const osg::Vec2f tileShift = osg::Vec2f(heightfield.mMinX, heightfield.mMinY) * scale; + const osg::Vec2f localShift = aabbShift + tileShift; + const float cellSize = static_cast(heightfield.mCellSize); + const osg::Vec3f cellShift( + heightfield.mCellPosition.x() * cellSize, + heightfield.mCellPosition.y() * cellSize, + (heightfield.mMinHeight + heightfield.mMaxHeight) * 0.5f + ); + return makeMesh(std::move(triangles), cellShift + osg::Vec3f(localShift.x(), localShift.y(), 0)); } RecastMeshBuilder::RecastMeshBuilder(const TileBounds& bounds) noexcept @@ -199,24 +206,25 @@ namespace DetourNavigator } } - void RecastMeshBuilder::addWater(const int cellSize, const osg::Vec3f& shift) + void RecastMeshBuilder::addWater(const osg::Vec2i& cellPosition, const Water& water) { - mWater.push_back(Cell {cellSize, shift}); + mWater.push_back(CellWater {cellPosition, water}); } - void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, float height) + void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height) { - if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift))) - mFlatHeightfields.emplace_back(FlatHeightfield {*intersection, height + shift.z()}); + if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize))) + mFlatHeightfields.emplace_back(FlatHeightfield {cellPosition, cellSize, height}); } - void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights, + void RecastMeshBuilder::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights, std::size_t size, float minHeight, float maxHeight) { - const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift)); + const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellPosition, cellSize)); if (!intersection.has_value()) return; - const float stepSize = static_cast(cellSize) / (size - 1); + const osg::Vec3f shift = Misc::Convert::toOsg(BulletHelpers::getHeightfieldShift(cellPosition.x(), cellPosition.y(), cellSize, minHeight, maxHeight)); + const float stepSize = getHeightfieldScale(cellSize, size); const int halfCellSize = cellSize / 2; const auto local = [&] (float v, float shift) { return (v - shift + halfCellSize) / stepSize; }; const auto index = [&] (float v, int add) { return std::clamp(static_cast(v) + add, 0, size); }; @@ -235,14 +243,16 @@ namespace DetourNavigator for (std::size_t x = minX; x < endX; ++x) tileHeights.push_back(heights[x + y * size]); Heightfield heightfield; - heightfield.mBounds = *intersection; + heightfield.mCellPosition = cellPosition; + heightfield.mCellSize = cellSize; heightfield.mLength = static_cast(endY - minY); heightfield.mMinHeight = minHeight; heightfield.mMaxHeight = maxHeight; - heightfield.mShift = shift + osg::Vec3f(minX, minY, 0) * stepSize - osg::Vec3f(halfCellSize, halfCellSize, 0); - heightfield.mScale = stepSize; heightfield.mHeights = std::move(tileHeights); - mHeightfields.emplace_back(heightfield); + heightfield.mOriginalSize = size; + heightfield.mMinX = static_cast(minX); + heightfield.mMinY = static_cast(minY); + mHeightfields.push_back(std::move(heightfield)); } std::shared_ptr RecastMeshBuilder::create(std::size_t generation, std::size_t revision) && diff --git a/components/detournavigator/recastmeshbuilder.hpp b/components/detournavigator/recastmeshbuilder.hpp index 120e4b045a..4bdcb788e3 100644 --- a/components/detournavigator/recastmeshbuilder.hpp +++ b/components/detournavigator/recastmeshbuilder.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -49,11 +48,11 @@ namespace DetourNavigator void addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType); - void addWater(const int mCellSize, const osg::Vec3f& shift); + void addWater(const osg::Vec2i& cellPosition, const Water& water); - void addHeightfield(int cellSize, const osg::Vec3f& shift, float height); + void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, float height); - void addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights, std::size_t size, + void addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const float* heights, std::size_t size, float minHeight, float maxHeight); std::shared_ptr create(std::size_t generation, std::size_t revision) &&; @@ -61,7 +60,7 @@ namespace DetourNavigator private: const TileBounds mBounds; std::vector mTriangles; - std::vector mWater; + std::vector mWater; std::vector mHeightfields; std::vector mFlatHeightfields; diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp index 2eeb90a9b5..4772ec74a9 100644 --- a/components/detournavigator/recastmeshmanager.cpp +++ b/components/detournavigator/recastmeshmanager.cpp @@ -4,6 +4,7 @@ #include "heightfieldshape.hpp" #include +#include #include @@ -11,26 +12,26 @@ namespace { struct AddHeightfield { - const DetourNavigator::Cell& mCell; + osg::Vec2i mCellPosition; + int mCellSize; DetourNavigator::RecastMeshBuilder& mBuilder; void operator()(const DetourNavigator::HeightfieldSurface& v) { - mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight); + mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight); } void operator()(DetourNavigator::HeightfieldPlane v) { - mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeight); + mBuilder.addHeightfield(mCellPosition, mCellSize, v.mHeight); } }; } namespace DetourNavigator { - RecastMeshManager::RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation) - : mSettings(settings) - , mGeneration(generation) + RecastMeshManager::RecastMeshManager(const TileBounds& bounds, std::size_t generation) + : mGeneration(generation) , mTileBounds(bounds) { } @@ -74,55 +75,52 @@ namespace DetourNavigator return result; } - bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift) + bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { const std::lock_guard lock(mMutex); - if (!mWater.emplace(cellPosition, Cell {cellSize, shift}).second) + if (!mWater.emplace(cellPosition, Water {cellSize, level}).second) return false; ++mRevision; return true; } - std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const std::lock_guard lock(mMutex); const auto water = mWater.find(cellPosition); if (water == mWater.end()) return std::nullopt; ++mRevision; - const Cell result = water->second; + Water result = water->second; mWater.erase(water); return result; } - bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape) { const std::lock_guard lock(mMutex); - if (!mHeightfields.emplace(cellPosition, Heightfield {Cell {cellSize, shift}, shape}).second) + if (!mHeightfields.emplace(cellPosition, SizedHeightfieldShape {cellSize, shape}).second) return false; ++mRevision; return true; } - std::optional RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + std::optional RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) { const std::lock_guard lock(mMutex); const auto it = mHeightfields.find(cellPosition); if (it == mHeightfields.end()) return std::nullopt; ++mRevision; - const auto result = std::make_optional(it->second.mCell); + auto result = std::make_optional(it->second); mHeightfields.erase(it); return result; } std::shared_ptr RecastMeshManager::getMesh() const { - TileBounds tileBounds = mTileBounds; - tileBounds.mMin /= mSettings.mRecastScaleFactor; - tileBounds.mMax /= mSettings.mRecastScaleFactor; - RecastMeshBuilder builder(tileBounds); + RecastMeshBuilder builder(mTileBounds); using Object = std::tuple< osg::ref_ptr, std::reference_wrapper, @@ -134,9 +132,9 @@ namespace DetourNavigator { const std::lock_guard lock(mMutex); for (const auto& [k, v] : mWater) - builder.addWater(v.mSize, v.mShift); + builder.addWater(k, v); for (const auto& [cellPosition, v] : mHeightfields) - std::visit(AddHeightfield {v.mCell, builder}, v.mShape); + std::visit(AddHeightfield {cellPosition, v.mCellSize, builder}, v.mShape); objects.reserve(mObjects.size()); for (const auto& [k, object] : mObjects) { diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp index e1c1567cb3..e897c797fc 100644 --- a/components/detournavigator/recastmeshmanager.hpp +++ b/components/detournavigator/recastmeshmanager.hpp @@ -14,8 +14,6 @@ #include #include #include -#include -#include #include class btCollisionShape; @@ -31,10 +29,16 @@ namespace DetourNavigator btTransform mTransform; }; + struct SizedHeightfieldShape + { + int mCellSize; + HeightfieldShape mShape; + }; + class RecastMeshManager { public: - RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); + explicit RecastMeshManager(const TileBounds& bounds, std::size_t generation); bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); @@ -43,14 +47,13 @@ namespace DetourNavigator std::optional removeObject(const ObjectId id); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); - std::optional removeWater(const osg::Vec2i& cellPosition); + std::optional removeWater(const osg::Vec2i& cellPosition); - bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); - std::optional removeHeightfield(const osg::Vec2i& cellPosition); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); std::shared_ptr getMesh() const; @@ -67,20 +70,13 @@ namespace DetourNavigator Version mNavMeshVersion; }; - struct Heightfield - { - Cell mCell; - HeightfieldShape mShape; - }; - - const Settings& mSettings; const std::size_t mGeneration; const TileBounds mTileBounds; mutable std::mutex mMutex; std::size_t mRevision = 0; std::map mObjects; - std::map mWater; - std::map mHeightfields; + std::map mWater; + std::map mHeightfields; std::optional mLastNavMeshReportedChange; std::optional mLastNavMeshReport; }; diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index 8b4bc2fd6f..31aa13a208 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -11,7 +11,7 @@ namespace DetourNavigator namespace { bool updateCompoundObject(const btCompoundShape& shape, const AreaType areaType, - std::vector& children) + std::vector& children) { assert(static_cast(shape.getNumChildShapes()) == children.size()); bool result = false; @@ -23,39 +23,33 @@ namespace DetourNavigator return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCompoundShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) { - std::vector result; + std::vector result; for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - { - const CollisionShape collisionShape {holder, *shape.getChildShape(i)}; - result.emplace_back(collisionShape, shape.getChildTransform(i), areaType); - } + result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); return result; } - std::vector makeChildrenObjects(const osg::ref_ptr& holder, - const btCollisionShape& shape, const AreaType areaType) + std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) { if (shape.isCompound()) - return makeChildrenObjects(holder, static_cast(shape), areaType); - return std::vector(); + return makeChildrenObjects(static_cast(shape), areaType); + return {}; } } - RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + ChildRecastMeshObject::ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType) - : mHolder(shape.getHolder()) - , mShape(shape.getShape()) + : mShape(shape) , mTransform(transform) , mAreaType(areaType) - , mLocalScaling(mShape.get().getLocalScaling()) - , mChildren(makeChildrenObjects(mHolder, mShape.get(), mAreaType)) + , mLocalScaling(shape.getLocalScaling()) + , mChildren(makeChildrenObjects(shape, mAreaType)) { } - bool RecastMeshObject::update(const btTransform& transform, const AreaType areaType) + bool ChildRecastMeshObject::update(const btTransform& transform, const AreaType areaType) { bool result = false; if (!(mTransform == transform)) @@ -78,4 +72,11 @@ namespace DetourNavigator || result; return result; } + + RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, + const AreaType areaType) + : mHolder(shape.getHolder()) + , mImpl(shape.getShape(), transform, areaType) + { + } } diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index 0c50c2f346..e833ee37e3 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -32,40 +32,45 @@ namespace DetourNavigator std::reference_wrapper mShape; }; - class RecastMeshObject + class ChildRecastMeshObject { public: - RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType); + ChildRecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); bool update(const btTransform& transform, const AreaType areaType); - const osg::ref_ptr& getHolder() const - { - return mHolder; - } - - const btCollisionShape& getShape() const - { - return mShape; - } + const btCollisionShape& getShape() const { return mShape; } - const btTransform& getTransform() const - { - return mTransform; - } + const btTransform& getTransform() const { return mTransform; } - AreaType getAreaType() const - { - return mAreaType; - } + AreaType getAreaType() const { return mAreaType; } private: - osg::ref_ptr mHolder; std::reference_wrapper mShape; btTransform mTransform; AreaType mAreaType; btVector3 mLocalScaling; - std::vector mChildren; + std::vector mChildren; + }; + + class RecastMeshObject + { + public: + RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType); + + bool update(const btTransform& transform, const AreaType areaType) { return mImpl.update(transform, areaType); } + + const osg::ref_ptr& getHolder() const { return mHolder; } + + const btCollisionShape& getShape() const { return mImpl.getShape(); } + + const btTransform& getTransform() const { return mImpl.getTransform(); } + + AreaType getAreaType() const { return mImpl.getAreaType(); } + + private: + osg::ref_ptr mHolder; + ChildRecastMeshObject mImpl; }; } diff --git a/components/detournavigator/serialization/binaryreader.hpp b/components/detournavigator/serialization/binaryreader.hpp new file mode 100644 index 0000000000..0d75c3ac99 --- /dev/null +++ b/components/detournavigator/serialization/binaryreader.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYREADER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + class BinaryReader + { + public: + explicit BinaryReader(const std::byte* pos, const std::byte* end) + : mPos(pos), mEnd(end) + { + assert(mPos <= mEnd); + } + + BinaryReader(const BinaryReader&) = delete; + + template + void operator()(Format&& format, T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(sizeof(value))) + throw std::runtime_error("Not enough data"); + std::memcpy(&value, mPos, sizeof(value)); + mPos += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mPos < static_cast(count * sizeof(T))) + throw std::runtime_error("Not enough data"); + const std::size_t size = sizeof(T) * count; + std::memcpy(data, mPos, size); + mPos += size; + } + else + { + format(*this, data, count); + } + } + + private: + const std::byte* mPos; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/binarywriter.hpp b/components/detournavigator/serialization/binarywriter.hpp new file mode 100644 index 0000000000..5e710d85d5 --- /dev/null +++ b/components/detournavigator/serialization/binarywriter.hpp @@ -0,0 +1,62 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_BINARYWRITER_H + +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + struct BinaryWriter + { + public: + explicit BinaryWriter(std::byte* dest, const std::byte* end) + : mDest(dest), mEnd(end) + { + assert(mDest <= mEnd); + } + + BinaryWriter(const BinaryWriter&) = delete; + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + { + if (mEnd - mDest < static_cast(sizeof(value))) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, &value, sizeof(value)); + mDest += sizeof(value); + } + else + { + format(*this, value); + } + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + { + const std::size_t size = sizeof(T) * count; + if (mEnd - mDest < static_cast(size)) + throw std::runtime_error("Not enough space"); + std::memcpy(mDest, data, size); + mDest += size; + } + else + { + format(*this, data, count); + } + } + + private: + std::byte* mDest; + const std::byte* const mEnd; + }; +} + +#endif diff --git a/components/detournavigator/serialization/format.hpp b/components/detournavigator/serialization/format.hpp new file mode 100644 index 0000000000..d07ab9da6f --- /dev/null +++ b/components/detournavigator/serialization/format.hpp @@ -0,0 +1,80 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_FORMAT_H + +#include +#include +#include +#include +#include +#include + +namespace DetourNavigator::Serialization +{ + enum class Mode + { + Read, + Write, + }; + + template + struct IsContiguousContainer : std::false_type {}; + + template + struct IsContiguousContainer> : std::true_type {}; + + template + constexpr bool isContiguousContainer = IsContiguousContainer>::value; + + template + struct Format + { + template + void operator()(Visitor&& visitor, T* data, std::size_t size) const + { + if constexpr (std::is_arithmetic_v) + { + visitor(self(), data, size); + } + else if constexpr (std::is_enum_v) + { + if constexpr (mode == Mode::Write) + visitor(self(), reinterpret_cast*>(data), size); + else + { + static_assert(mode == Mode::Read); + visitor(self(), reinterpret_cast*>(data), size); + } + } + else + { + std::for_each(data, data + size, [&] (auto& v) { visitor(self(), v); }); + } + } + + template + void operator()(Visitor&& visitor, T(& data)[size]) const + { + self()(std::forward(visitor), data, size); + } + + template + auto operator()(Visitor&& visitor, T&& value) const + -> std::enable_if_t> + { + if constexpr (mode == Mode::Write) + visitor(self(), value.size()); + else + { + static_assert(mode == Mode::Read); + std::size_t size = 0; + visitor(self(), size); + value.resize(size); + } + self()(std::forward(visitor), value.data(), value.size()); + } + + const Derived& self() const { return static_cast(*this); } + }; +} + +#endif diff --git a/components/detournavigator/serialization/sizeaccumulator.hpp b/components/detournavigator/serialization/sizeaccumulator.hpp new file mode 100644 index 0000000000..28bdb5c1cb --- /dev/null +++ b/components/detournavigator/serialization/sizeaccumulator.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_SERIALIZATION_SIZEACCUMULATOR_H + +#include +#include + +namespace DetourNavigator::Serialization +{ + class SizeAccumulator + { + public: + SizeAccumulator() = default; + + SizeAccumulator(const SizeAccumulator&) = delete; + + std::size_t value() const { return mValue; } + + template + void operator()(Format&& format, const T& value) + { + if constexpr (std::is_arithmetic_v) + mValue += sizeof(T); + else + format(*this, value); + } + + template + auto operator()(Format&& format, const T* data, std::size_t count) + { + if constexpr (std::is_arithmetic_v) + mValue += count * sizeof(T); + else + format(*this, data, count); + } + + private: + std::size_t mValue = 0; + }; +} + +#endif diff --git a/components/detournavigator/settings.cpp b/components/detournavigator/settings.cpp index ff99fae20c..e428f3695a 100644 --- a/components/detournavigator/settings.cpp +++ b/components/detournavigator/settings.cpp @@ -1,14 +1,12 @@ #include "settings.hpp" #include +#include namespace DetourNavigator { - std::optional makeSettingsFromSettingsManager() + Settings makeSettingsFromSettingsManager() { - if (!::Settings::Manager::getBool("enable", "Navigator")) - return std::optional(); - Settings navigatorSettings; navigatorSettings.mBorderSize = ::Settings::Manager::getInt("border size", "Navigator"); @@ -16,9 +14,9 @@ namespace DetourNavigator navigatorSettings.mCellSize = ::Settings::Manager::getFloat("cell size", "Navigator"); navigatorSettings.mDetailSampleDist = ::Settings::Manager::getFloat("detail sample dist", "Navigator"); navigatorSettings.mDetailSampleMaxError = ::Settings::Manager::getFloat("detail sample max error", "Navigator"); - navigatorSettings.mMaxClimb = 0; + navigatorSettings.mMaxClimb = Constants::sStepSizeUp; navigatorSettings.mMaxSimplificationError = ::Settings::Manager::getFloat("max simplification error", "Navigator"); - navigatorSettings.mMaxSlope = 0; + navigatorSettings.mMaxSlope = Constants::sMaxSlope; navigatorSettings.mRecastScaleFactor = ::Settings::Manager::getFloat("recast scale factor", "Navigator"); navigatorSettings.mSwimHeightScale = 0; navigatorSettings.mMaxEdgeLen = ::Settings::Manager::getInt("max edge len", "Navigator"); diff --git a/components/detournavigator/settings.hpp b/components/detournavigator/settings.hpp index 39f5815b8e..0ea35d9b49 100644 --- a/components/detournavigator/settings.hpp +++ b/components/detournavigator/settings.hpp @@ -2,7 +2,6 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_SETTINGS_H #include -#include #include namespace DetourNavigator @@ -41,7 +40,7 @@ namespace DetourNavigator std::chrono::milliseconds mMinUpdateInterval; }; - std::optional makeSettingsFromSettingsManager(); + Settings makeSettingsFromSettingsManager(); } #endif diff --git a/components/detournavigator/settingsutils.hpp b/components/detournavigator/settingsutils.hpp index 258eb64c65..6f15faaa3e 100644 --- a/components/detournavigator/settingsutils.hpp +++ b/components/detournavigator/settingsutils.hpp @@ -96,6 +96,17 @@ namespace DetourNavigator { return std::floor(std::sqrt(settings.mMaxTilesNumber / osg::PI)) - 1; } + + inline TileBounds makeRealTileBoundsWithBorder(const Settings& settings, const TilePosition& tilePosition) + { + TileBounds result = makeTileBounds(settings, tilePosition); + const float border = getBorderSize(settings); + result.mMin -= osg::Vec2f(border, border); + result.mMax += osg::Vec2f(border, border); + result.mMin /= settings.mRecastScaleFactor; + result.mMax /= settings.mRecastScaleFactor; + return result; + } } #endif diff --git a/components/detournavigator/tilebounds.hpp b/components/detournavigator/tilebounds.hpp index 8557045342..693a382740 100644 --- a/components/detournavigator/tilebounds.hpp +++ b/components/detournavigator/tilebounds.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_DETOURNAVIGATOR_TILEBOUNDS_H #include +#include #include #include @@ -32,6 +33,14 @@ namespace DetourNavigator return std::nullopt; return TileBounds {osg::Vec2f(minX, minY), osg::Vec2f(maxX, maxY)}; } + + inline TileBounds maxCellTileBounds(const osg::Vec2i& position, int size) + { + return TileBounds { + osg::Vec2f(position.x(), position.y()) * size, + osg::Vec2f(position.x() + 1, position.y() + 1) * size + }; + } } #endif diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index 63d7e13f6b..20fbe30667 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -18,12 +18,11 @@ namespace DetourNavigator const btTransform& transform, const AreaType areaType) { std::vector tilesPositions; - const auto border = getBorderSize(mSettings); { auto tiles = mTiles.lock(); getTilesPositions(shape.getShape(), transform, mSettings, [&] (const TilePosition& tilePosition) { - if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get())) + if (addTile(id, shape, transform, areaType, tilePosition, tiles.get())) tilesPositions.push_back(tilePosition); }); } @@ -55,11 +54,8 @@ namespace DetourNavigator return result; } - bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const osg::Vec3f& shift) + bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, int cellSize, float level) { - const auto border = getBorderSize(mSettings); - auto& tilesPositions = mWaterTilesPositions[cellPosition]; bool result = false; @@ -69,7 +65,7 @@ namespace DetourNavigator const auto tiles = mTiles.lock(); for (auto& tile : *tiles) { - if (tile.second->addWater(cellPosition, cellSize, shift)) + if (tile.second->addWater(cellPosition, cellSize, level)) { tilesPositions.push_back(tile.first); result = true; @@ -78,19 +74,18 @@ namespace DetourNavigator } else { + const btVector3 shift = Misc::Convert::toBullet(getWaterShift3d(cellPosition, cellSize, level)); getTilesPositions(cellSize, shift, mSettings, [&] (const TilePosition& tilePosition) { const auto tiles = mTiles.lock(); auto tile = tiles->find(tilePosition); if (tile == tiles->end()) { - auto tileBounds = makeTileBounds(mSettings, tilePosition); - tileBounds.mMin -= osg::Vec2f(border, border); - tileBounds.mMax += osg::Vec2f(border, border); - tile = tiles->insert(std::make_pair(tilePosition, - std::make_shared(mSettings, tileBounds, mTilesGeneration))).first; + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = tiles->emplace(tilePosition, + std::make_shared(tileBounds, mTilesGeneration)).first; } - if (tile->second->addWater(cellPosition, cellSize, shift)) + if (tile->second->addWater(cellPosition, cellSize, level)) { tilesPositions.push_back(tilePosition); result = true; @@ -104,12 +99,12 @@ namespace DetourNavigator return result; } - std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto object = mWaterTilesPositions.find(cellPosition); if (object == mWaterTilesPositions.end()) return std::nullopt; - std::optional result; + std::optional result; for (const auto& tilePosition : object->second) { const auto tiles = mTiles.lock(); @@ -131,10 +126,9 @@ namespace DetourNavigator } bool TileCachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, - const osg::Vec3f& shift, const HeightfieldShape& shape) + const HeightfieldShape& shape) { - const auto border = getBorderSize(mSettings); - + const btVector3 shift = getHeightfieldShift(shape, cellPosition, cellSize); auto& tilesPositions = mHeightfieldTilesPositions[cellPosition]; bool result = false; @@ -145,13 +139,11 @@ namespace DetourNavigator auto tile = tiles->find(tilePosition); if (tile == tiles->end()) { - auto tileBounds = makeTileBounds(mSettings, tilePosition); - tileBounds.mMin -= osg::Vec2f(border, border); - tileBounds.mMax += osg::Vec2f(border, border); - tile = tiles->insert(std::make_pair(tilePosition, - std::make_shared(mSettings, tileBounds, mTilesGeneration))).first; + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = tiles->emplace(tilePosition, + std::make_shared(tileBounds, mTilesGeneration)).first; } - if (tile->second->addHeightfield(cellPosition, cellSize, shift, shape)) + if (tile->second->addHeightfield(cellPosition, cellSize, shape)) { tilesPositions.push_back(tilePosition); result = true; @@ -164,12 +156,12 @@ namespace DetourNavigator return result; } - std::optional TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + std::optional TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) { const auto object = mHeightfieldTilesPositions.find(cellPosition); if (object == mHeightfieldTilesPositions.end()) return std::nullopt; - std::optional result; + std::optional result; for (const auto& tilePosition : object->second) { const auto tiles = mTiles.lock(); @@ -192,17 +184,16 @@ namespace DetourNavigator std::shared_ptr TileCachedRecastMeshManager::getMesh(const TilePosition& tilePosition) const { - const auto manager = [&] () -> std::shared_ptr - { - const auto tiles = mTiles.lockConst(); - const auto it = tiles->find(tilePosition); - if (it == tiles->end()) - return nullptr; - return it->second; - } (); - if (manager == nullptr) - return nullptr; - return manager->getMesh(); + if (const auto manager = getManager(tilePosition)) + return manager->getMesh(); + return nullptr; + } + + std::shared_ptr TileCachedRecastMeshManager::getCachedMesh(const TilePosition& tilePosition) const + { + if (const auto manager = getManager(tilePosition)) + return manager->getCachedMesh(); + return nullptr; } std::size_t TileCachedRecastMeshManager::getRevision() const @@ -220,17 +211,15 @@ namespace DetourNavigator } bool TileCachedRecastMeshManager::addTile(const ObjectId id, const CollisionShape& shape, - const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border, + const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles) { auto tile = tiles.find(tilePosition); if (tile == tiles.end()) { - auto tileBounds = makeTileBounds(mSettings, tilePosition); - tileBounds.mMin -= osg::Vec2f(border, border); - tileBounds.mMax += osg::Vec2f(border, border); - tile = tiles.insert(std::make_pair( - tilePosition, std::make_shared(mSettings, tileBounds, mTilesGeneration))).first; + const TileBounds tileBounds = makeRealTileBoundsWithBorder(mSettings, tilePosition); + tile = tiles.emplace(tilePosition, + std::make_shared(tileBounds, mTilesGeneration)).first; } return tile->second->addObject(id, shape, transform, areaType); } @@ -256,4 +245,13 @@ namespace DetourNavigator } return tileResult; } + + std::shared_ptr TileCachedRecastMeshManager::getManager(const TilePosition& tilePosition) const + { + const auto tiles = mTiles.lockConst(); + const auto it = tiles->find(tilePosition); + if (it == tiles->end()) + return nullptr; + return it->second; + } } diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index f6bc40d668..bb08a4227e 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -33,7 +33,6 @@ namespace DetourNavigator if (object == mObjectsTilesPositions.end()) return false; auto& currentTiles = object->second; - const auto border = getBorderSize(mSettings); bool changed = false; std::vector newTiles; { @@ -49,7 +48,7 @@ namespace DetourNavigator changed = true; } } - else if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get())) + else if (addTile(id, shape, transform, areaType, tilePosition, tiles.get())) { newTiles.push_back(tilePosition); onChangedTile(tilePosition); @@ -77,17 +76,18 @@ namespace DetourNavigator std::optional removeObject(const ObjectId id); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + bool addWater(const osg::Vec2i& cellPosition, int cellSize, float level); - std::optional removeWater(const osg::Vec2i& cellPosition); + std::optional removeWater(const osg::Vec2i& cellPosition); - bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, - const HeightfieldShape& shape); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const HeightfieldShape& shape); - std::optional removeHeightfield(const osg::Vec2i& cellPosition); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); std::shared_ptr getMesh(const TilePosition& tilePosition) const; + std::shared_ptr getCachedMesh(const TilePosition& tilePosition) const; + template void forEachTile(Function&& function) const { @@ -111,13 +111,15 @@ namespace DetourNavigator std::size_t mTilesGeneration = 0; bool addTile(const ObjectId id, const CollisionShape& shape, const btTransform& transform, - const AreaType areaType, const TilePosition& tilePosition, float border, TilesMap& tiles); + const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles); bool updateTile(const ObjectId id, const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles); std::optional removeTile(const ObjectId id, const TilePosition& tilePosition, TilesMap& tiles); + + inline std::shared_ptr getManager(const TilePosition& tilePosition) const; }; } diff --git a/components/detournavigator/version.hpp b/components/detournavigator/version.hpp index c9de98459d..792680a7d5 100644 --- a/components/detournavigator/version.hpp +++ b/components/detournavigator/version.hpp @@ -8,12 +8,32 @@ namespace DetourNavigator { struct Version { - std::size_t mGeneration; - std::size_t mRevision; + std::size_t mGeneration = 0; + std::size_t mRevision = 0; + + friend inline auto tie(const Version& value) + { + return std::tie(value.mGeneration, value.mRevision); + } friend inline bool operator<(const Version& lhs, const Version& rhs) { - return std::tie(lhs.mGeneration, lhs.mRevision) < std::tie(rhs.mGeneration, rhs.mRevision); + return tie(lhs) < tie(rhs); + } + + friend inline bool operator<=(const Version& lhs, const Version& rhs) + { + return tie(lhs) <= tie(rhs); + } + + friend inline bool operator==(const Version& lhs, const Version& rhs) + { + return tie(lhs) == tie(rhs); + } + + friend inline bool operator!=(const Version& lhs, const Version& rhs) + { + return !(lhs == rhs); } }; } diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 8b5f1f1946..a79366f9c2 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -22,7 +22,9 @@ namespace ESM Flag_None = 0, Flag_Applied = 1 << 0, Flag_Remove = 1 << 1, - Flag_Ignore_Resistances = 1 << 2 + Flag_Ignore_Resistances = 1 << 2, + Flag_Ignore_Reflect = 1 << 3, + Flag_Ignore_SpellAbsorption = 1 << 4 }; int mEffectId; diff --git a/components/esm/aipackage.hpp b/components/esm/aipackage.hpp index 026e65dd82..6993867da1 100644 --- a/components/esm/aipackage.hpp +++ b/components/esm/aipackage.hpp @@ -37,7 +37,8 @@ namespace ESM struct AITravel { float mX, mY, mZ; - int mUnk; + unsigned char mShouldRepeat; + unsigned char mPadding[3]; }; struct AITarget @@ -45,13 +46,14 @@ namespace ESM float mX, mY, mZ; short mDuration; NAME32 mId; - short mUnk; + unsigned char mShouldRepeat; + unsigned char mPadding; }; struct AIActivate { NAME32 mName; - unsigned char mUnk; + unsigned char mShouldRepeat; }; #pragma pack(pop) diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index ca4c21802c..b99cac3ade 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -3,6 +3,7 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include #include namespace ESM @@ -34,12 +35,16 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); esm.getHNOT (mHidden, "HIDD"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); } void AiTravel::save(ESMWriter &esm) const { esm.writeHNT ("DATA", mData); esm.writeHNT ("HIDD", mHidden); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiEscort::load(ESMReader &esm) @@ -50,6 +55,15 @@ namespace AiSequence esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); + if(esm.getFormat() < 18) + { + // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. + // The exact value of mDuration only matters for repeating packages. + // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. + mData.mDuration = std::max(mRemainingDuration > 0, mRemainingDuration); + } } void AiEscort::save(ESMWriter &esm) const @@ -60,6 +74,8 @@ namespace AiSequence esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiFollow::load(ESMReader &esm) @@ -75,6 +91,15 @@ namespace AiSequence esm.getHNOT (mCommanded, "CMND"); mActive = false; esm.getHNOT (mActive, "ACTV"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); + if(esm.getFormat() < 18) + { + // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. + // The exact value of mDuration only matters for repeating packages. + // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. + mData.mDuration = std::max(mRemainingDuration > 0, mRemainingDuration); + } } void AiFollow::save(ESMWriter &esm) const @@ -89,16 +114,22 @@ namespace AiSequence esm.writeHNT ("CMND", mCommanded); if (mActive) esm.writeHNT("ACTV", mActive); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiActivate::load(ESMReader &esm) { mTargetId = esm.getHNString("TARG"); + mRepeat = false; + esm.getHNOT(mRepeat, "REPT"); } void AiActivate::save(ESMWriter &esm) const { esm.writeHNString("TARG", mTargetId); + if(mRepeat) + esm.writeHNT("REPT", mRepeat); } void AiCombat::load(ESMReader &esm) @@ -166,6 +197,7 @@ namespace AiSequence void AiSequence::load(ESMReader &esm) { + int count = 0; while (esm.isNextSub("AIPK")) { int type; @@ -181,6 +213,7 @@ namespace AiSequence std::unique_ptr ptr = std::make_unique(); ptr->load(esm); mPackages.back().mPackage = ptr.release(); + ++count; break; } case Ai_Travel: @@ -188,6 +221,7 @@ namespace AiSequence std::unique_ptr ptr = std::make_unique(); ptr->load(esm); mPackages.back().mPackage = ptr.release(); + ++count; break; } case Ai_Escort: @@ -195,6 +229,7 @@ namespace AiSequence std::unique_ptr ptr = std::make_unique(); ptr->load(esm); mPackages.back().mPackage = ptr.release(); + ++count; break; } case Ai_Follow: @@ -202,6 +237,7 @@ namespace AiSequence std::unique_ptr ptr = std::make_unique(); ptr->load(esm); mPackages.back().mPackage = ptr.release(); + ++count; break; } case Ai_Activate: @@ -209,6 +245,7 @@ namespace AiSequence std::unique_ptr ptr = std::make_unique(); ptr->load(esm); mPackages.back().mPackage = ptr.release(); + ++count; break; } case Ai_Combat: @@ -231,6 +268,23 @@ namespace AiSequence } esm.getHNOT (mLastAiPackage, "LAST"); + + if(count > 1 && esm.getFormat() < 18) + { + for(auto& pkg : mPackages) + { + if(pkg.mType == Ai_Wander) + static_cast(pkg.mPackage)->mData.mShouldRepeat = true; + else if(pkg.mType == Ai_Travel) + static_cast(pkg.mPackage)->mRepeat = true; + else if(pkg.mType == Ai_Escort) + static_cast(pkg.mPackage)->mRepeat = true; + else if(pkg.mType == Ai_Follow) + static_cast(pkg.mPackage)->mRepeat = true; + else if(pkg.mType == Ai_Activate) + static_cast(pkg.mPackage)->mRepeat = true; + } + } } } } diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index d8c20185f8..00c1316d9c 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -81,6 +81,7 @@ namespace ESM { AiTravelData mData; bool mHidden; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -94,6 +95,7 @@ namespace ESM std::string mTargetId; std::string mCellId; float mRemainingDuration; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -112,6 +114,7 @@ namespace ESM bool mCommanded; bool mActive; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; @@ -120,6 +123,7 @@ namespace ESM struct AiActivate : AiPackage { std::string mTargetId; + bool mRepeat; void load(ESMReader &esm); void save(ESMWriter &esm) const; diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index 81e0b9f557..002a885d92 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -5,11 +5,6 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM -{ - int GroundcoverIndex = std::numeric_limits::max(); -} - void ESM::RefNum::load (ESMReader& esm, bool wide, const std::string& tag) { if (wide) @@ -66,7 +61,7 @@ void ESM::CellRef::loadData(ESMReader &esm, bool &isDeleted) while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'U','N','A','M'>::value: esm.getHT(mReferenceBlocked); diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp index f6eff24cbf..0013329ccc 100644 --- a/components/esm/cellref.hpp +++ b/components/esm/cellref.hpp @@ -12,7 +12,6 @@ namespace ESM class ESMReader; const int UnbreakableLock = std::numeric_limits::max(); - extern int GroundcoverIndex; struct RefNum { @@ -27,10 +26,6 @@ namespace ESM inline bool isSet() const { return mIndex != 0 || mContentFile != -1; } inline void unset() { *this = {0, -1}; } - - // Note: this method should not be used for objects with invalid RefNum - // (for example, for objects from disabled plugins in savegames). - inline bool fromGroundcoverFile() const { return mContentFile >= GroundcoverIndex; } }; /* Cell reference. This represents ONE object (of many) inside the diff --git a/components/esm/debugprofile.cpp b/components/esm/debugprofile.cpp index 6276258c48..090d2bfe6d 100644 --- a/components/esm/debugprofile.cpp +++ b/components/esm/debugprofile.cpp @@ -14,7 +14,7 @@ void ESM::DebugProfile::load (ESMReader& esm, bool &isDeleted) while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/defs.hpp b/components/esm/defs.hpp index 7f2fe19cc5..254e66ec3a 100644 --- a/components/esm/defs.hpp +++ b/components/esm/defs.hpp @@ -165,6 +165,7 @@ enum RecNameInts // format 1 REC_FILT = FourCC<'F','I','L','T'>::value, REC_DBGP = FourCC<'D','B','G','P'>::value, ///< only used in project files + REC_LUAL = FourCC<'L','U','A','L'>::value, // LuaScriptsCfg // format 16 - Lua scripts in saved games REC_LUAM = FourCC<'L','U','A','M'>::value, // LuaManager data diff --git a/components/esm/esmcommon.hpp b/components/esm/esmcommon.hpp index 10e05de905..7caaae2afb 100644 --- a/components/esm/esmcommon.hpp +++ b/components/esm/esmcommon.hpp @@ -102,25 +102,35 @@ struct FIXED_STRING : public FIXED_STRING_BASE template <> struct FIXED_STRING<4> : public FIXED_STRING_BASE { - union { - char data[4]; - uint32_t intval; - }; + char data[4]; using FIXED_STRING_BASE::operator==; using FIXED_STRING_BASE::operator!=; - bool operator==(uint32_t v) const { return v == intval; } - bool operator!=(uint32_t v) const { return v != intval; } + bool operator==(uint32_t v) const { return v == toInt(); } + bool operator!=(uint32_t v) const { return v != toInt(); } + + FIXED_STRING<4>& operator=(std::uint32_t value) + { + std::memcpy(data, &value, sizeof(data)); + return *this; + } void assign(const std::string& value) { - intval = 0; + std::memset(data, 0, sizeof(data)); std::memcpy(data, value.data(), std::min(value.size(), sizeof(data))); } char const* ro_data() const { return data; } char* rw_data() { return data; } + + std::uint32_t toInt() const + { + std::uint32_t value; + std::memcpy(&value, data, sizeof(data)); + return value; + } }; typedef FIXED_STRING<4> NAME; diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index dbf713315b..c84a1798ff 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -1,5 +1,8 @@ #include "esmreader.hpp" +#include +#include + #include namespace ESM @@ -17,11 +20,11 @@ ESM_Context ESMReader::getContext() ESMReader::ESMReader() : mRecordFlags(0) , mBuffer(50*1024) - , mGlobalReaderList(nullptr) , mEncoder(nullptr) , mFileSize(0) { clearCtx(); + mCtx.index = 0; } void ESMReader::restoreContext(const ESM_Context &rc) @@ -55,6 +58,29 @@ void ESMReader::clearCtx() mCtx.subName.clear(); } +void ESMReader::resolveParentFileIndices(const std::vector& allPlugins) +{ + mCtx.parentFileIndices.clear(); + const std::vector &masters = getGameFiles(); + for (size_t j = 0; j < masters.size(); j++) { + const Header::MasterData &mast = masters[j]; + std::string fname = mast.name; + int index = getIndex(); + for (int i = 0; i < getIndex(); i++) { + const ESMReader& reader = allPlugins.at(i); + if (reader.getFileSize() == 0) + continue; // Content file in non-ESM format + const std::string candidate = reader.getName(); + std::string fnamecandidate = boost::filesystem::path(candidate).filename().string(); + if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) { + index = i; + break; + } + } + mCtx.parentFileIndices.push_back(index); + } +} + void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name) { close(); diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index a438dca0cd..d7eb6ff0a1 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -32,14 +32,14 @@ public: int getVer() const { return mHeader.mData.version; } int getRecordCount() const { return mHeader.mData.records; } float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; } - const std::string getAuthor() const { return mHeader.mData.author; } - const std::string getDesc() const { return mHeader.mData.desc; } + const std::string& getAuthor() const { return mHeader.mData.author; } + const std::string& getDesc() const { return mHeader.mData.desc; } const std::vector &getGameFiles() const { return mHeader.mMaster; } const Header& getHeader() const { return mHeader; } int getFormat() const { return mHeader.mFormat; }; const NAME &retSubName() const { return mCtx.subName; } uint32_t getSubSize() const { return mCtx.leftSub; } - std::string getName() const {return mCtx.filename; }; + const std::string& getName() const { return mCtx.filename; }; /************************************************************************* * @@ -80,13 +80,15 @@ public: // to the individual load() methods. This hack allows to pass this reference // indirectly to the load() method. void setIndex(const int index) { mCtx.index = index;} - int getIndex() {return mCtx.index;} + int getIndex() const {return mCtx.index;} - void setGlobalReaderList(std::vector *list) {mGlobalReaderList = list;} - std::vector *getGlobalReaderList() {return mGlobalReaderList;} - - void addParentFileIndex(int index) { mCtx.parentFileIndices.push_back(index); } + // Assign parent esX files by tracking their indices in the global list of + // all files/readers used by the engine. This is required for correct adjustRefNum() results + // as required for handling moved, deleted and edited CellRefs. + /// @note Does not validate. + void resolveParentFileIndices(const std::vector& files); const std::vector& getParentFileIndices() const { return mCtx.parentFileIndices; } + bool isValidParentFileIndex(int i) const { return i != getIndex(); } /************************************************************************* * @@ -279,7 +281,6 @@ private: Header mHeader; - std::vector *mGlobalReaderList; ToUTF8::Utf8Encoder* mEncoder; size_t mFileSize; diff --git a/components/esm/filter.cpp b/components/esm/filter.cpp index 8d1b755055..76a518c63d 100644 --- a/components/esm/filter.cpp +++ b/components/esm/filter.cpp @@ -14,7 +14,7 @@ void ESM::Filter::load (ESMReader& esm, bool &isDeleted) while (esm.hasMoreSubs()) { esm.getSubName(); - uint32_t name = esm.retSubName().intval; + uint32_t name = esm.retSubName().toInt(); switch (name) { case ESM::SREC_NAME: diff --git a/components/esm/loadacti.cpp b/components/esm/loadacti.cpp index 6a08bca749..fcb0954918 100644 --- a/components/esm/loadacti.cpp +++ b/components/esm/loadacti.cpp @@ -17,7 +17,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadacti.hpp b/components/esm/loadacti.hpp index 5b88ad379f..c0cf274ed9 100644 --- a/components/esm/loadacti.hpp +++ b/components/esm/loadacti.hpp @@ -13,7 +13,7 @@ struct Activator { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Activator"; } + static std::string_view getRecordType() { return "Activator"; } unsigned int mRecordFlags; std::string mId, mName, mScript, mModel; diff --git a/components/esm/loadalch.cpp b/components/esm/loadalch.cpp index b7c81ebdb4..ad30570f74 100644 --- a/components/esm/loadalch.cpp +++ b/components/esm/loadalch.cpp @@ -20,7 +20,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadalch.hpp b/components/esm/loadalch.hpp index d573531134..e032464abe 100644 --- a/components/esm/loadalch.hpp +++ b/components/esm/loadalch.hpp @@ -20,7 +20,7 @@ struct Potion static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Potion"; } + static std::string_view getRecordType() { return "Potion"; } struct ALDTstruct { diff --git a/components/esm/loadappa.cpp b/components/esm/loadappa.cpp index e9ff3ea86c..1113870197 100644 --- a/components/esm/loadappa.cpp +++ b/components/esm/loadappa.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadappa.hpp b/components/esm/loadappa.hpp index fcd9100be4..026a471f5b 100644 --- a/components/esm/loadappa.hpp +++ b/components/esm/loadappa.hpp @@ -17,7 +17,7 @@ struct Apparatus { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Apparatus"; } + static std::string_view getRecordType() { return "Apparatus"; } enum AppaType { diff --git a/components/esm/loadarmo.cpp b/components/esm/loadarmo.cpp index 902d4a4467..cab0d52a88 100644 --- a/components/esm/loadarmo.cpp +++ b/components/esm/loadarmo.cpp @@ -50,7 +50,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadarmo.hpp b/components/esm/loadarmo.hpp index 195230fbf2..f11e3509d5 100644 --- a/components/esm/loadarmo.hpp +++ b/components/esm/loadarmo.hpp @@ -67,7 +67,7 @@ struct Armor { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Armor"; } + static std::string_view getRecordType() { return "Armor"; } enum Type { diff --git a/components/esm/loadbody.cpp b/components/esm/loadbody.cpp index 239cff7c8b..c7f6bce40a 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm/loadbody.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadbody.hpp b/components/esm/loadbody.hpp index 1be775ffec..145fe4b6f6 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm/loadbody.hpp @@ -13,7 +13,7 @@ struct BodyPart { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "BodyPart"; } + static std::string_view getRecordType() { return "BodyPart"; } enum MeshPart { diff --git a/components/esm/loadbook.cpp b/components/esm/loadbook.cpp index 756f01da9c..07b9a6b50f 100644 --- a/components/esm/loadbook.cpp +++ b/components/esm/loadbook.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadbook.hpp b/components/esm/loadbook.hpp index 60b03f7a36..e46bec6272 100644 --- a/components/esm/loadbook.hpp +++ b/components/esm/loadbook.hpp @@ -16,7 +16,7 @@ struct Book { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Book"; } + static std::string_view getRecordType() { return "Book"; } struct BKDTstruct { diff --git a/components/esm/loadbsgn.cpp b/components/esm/loadbsgn.cpp index 7514f1f85b..d767eb66e0 100644 --- a/components/esm/loadbsgn.cpp +++ b/components/esm/loadbsgn.cpp @@ -19,7 +19,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadbsgn.hpp b/components/esm/loadbsgn.hpp index 806323bf35..a199503a76 100644 --- a/components/esm/loadbsgn.hpp +++ b/components/esm/loadbsgn.hpp @@ -15,7 +15,7 @@ struct BirthSign { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "BirthSign"; } + static std::string_view getRecordType() { return "BirthSign"; } unsigned int mRecordFlags; std::string mId, mName, mDescription, mTexture; diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index d2cb23146f..b2c95ad25f 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -70,7 +70,7 @@ namespace ESM while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mName = esm.getHString(); @@ -117,7 +117,7 @@ namespace ESM while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'I','N','T','V'>::value: int waterl; diff --git a/components/esm/loadcell.hpp b/components/esm/loadcell.hpp index 18e929e13b..c2a694b744 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm/loadcell.hpp @@ -65,7 +65,7 @@ struct Cell { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Cell"; } + static std::string_view getRecordType() { return "Cell"; } enum Flags { diff --git a/components/esm/loadclas.cpp b/components/esm/loadclas.cpp index 7526fe4f52..c70f7dd0d3 100644 --- a/components/esm/loadclas.cpp +++ b/components/esm/loadclas.cpp @@ -48,7 +48,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadclas.hpp b/components/esm/loadclas.hpp index 1000879c4c..e1e8b2ff0f 100644 --- a/components/esm/loadclas.hpp +++ b/components/esm/loadclas.hpp @@ -19,7 +19,7 @@ struct Class { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Class"; } + static std::string_view getRecordType() { return "Class"; } enum AutoCalc { diff --git a/components/esm/loadclot.cpp b/components/esm/loadclot.cpp index cf03dbad36..8f2aff40fc 100644 --- a/components/esm/loadclot.cpp +++ b/components/esm/loadclot.cpp @@ -20,7 +20,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadclot.hpp b/components/esm/loadclot.hpp index bbc8449a95..26e82254ab 100644 --- a/components/esm/loadclot.hpp +++ b/components/esm/loadclot.hpp @@ -19,7 +19,7 @@ struct Clothing { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Clothing"; } + static std::string_view getRecordType() { return "Clothing"; } enum Type { diff --git a/components/esm/loadcont.cpp b/components/esm/loadcont.cpp index 03092571a4..b7757646a1 100644 --- a/components/esm/loadcont.cpp +++ b/components/esm/loadcont.cpp @@ -42,7 +42,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadcont.hpp b/components/esm/loadcont.hpp index ade9004214..eac791e3f3 100644 --- a/components/esm/loadcont.hpp +++ b/components/esm/loadcont.hpp @@ -37,7 +37,7 @@ struct Container { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Container"; } + static std::string_view getRecordType() { return "Container"; } enum Flags { diff --git a/components/esm/loadcrea.cpp b/components/esm/loadcrea.cpp index 02d664e1f2..590a68bc35 100644 --- a/components/esm/loadcrea.cpp +++ b/components/esm/loadcrea.cpp @@ -31,7 +31,7 @@ namespace ESM { while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadcrea.hpp b/components/esm/loadcrea.hpp index f6b188d96e..9d664d440e 100644 --- a/components/esm/loadcrea.hpp +++ b/components/esm/loadcrea.hpp @@ -23,7 +23,7 @@ struct Creature { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Creature"; } + static std::string_view getRecordType() { return "Creature"; } // Default is 0x48? enum Flags diff --git a/components/esm/loaddial.cpp b/components/esm/loaddial.cpp index 59fb13484b..535ea23380 100644 --- a/components/esm/loaddial.cpp +++ b/components/esm/loaddial.cpp @@ -28,7 +28,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'D','A','T','A'>::value: { diff --git a/components/esm/loaddial.hpp b/components/esm/loaddial.hpp index b80cbd74c3..7adc8b1cf8 100644 --- a/components/esm/loaddial.hpp +++ b/components/esm/loaddial.hpp @@ -22,7 +22,7 @@ struct Dialogue { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Dialogue"; } + static std::string_view getRecordType() { return "Dialogue"; } enum Type { diff --git a/components/esm/loaddoor.cpp b/components/esm/loaddoor.cpp index 3c446789b7..d5bf51b4da 100644 --- a/components/esm/loaddoor.cpp +++ b/components/esm/loaddoor.cpp @@ -17,7 +17,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loaddoor.hpp b/components/esm/loaddoor.hpp index 167eda20f5..84dbcbfa56 100644 --- a/components/esm/loaddoor.hpp +++ b/components/esm/loaddoor.hpp @@ -13,7 +13,7 @@ struct Door { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Door"; } + static std::string_view getRecordType() { return "Door"; } unsigned int mRecordFlags; std::string mId, mName, mModel, mScript, mOpenSound, mCloseSound; diff --git a/components/esm/loadench.cpp b/components/esm/loadench.cpp index ed3de90b50..db0727099c 100644 --- a/components/esm/loadench.cpp +++ b/components/esm/loadench.cpp @@ -19,7 +19,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadench.hpp b/components/esm/loadench.hpp index a4e1e8362c..3f094f754c 100644 --- a/components/esm/loadench.hpp +++ b/components/esm/loadench.hpp @@ -19,7 +19,7 @@ struct Enchantment { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Enchantment"; } + static std::string_view getRecordType() { return "Enchantment"; } enum Type { diff --git a/components/esm/loadfact.cpp b/components/esm/loadfact.cpp index b71348de44..61d0e1dcf4 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm/loadfact.cpp @@ -41,7 +41,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadfact.hpp b/components/esm/loadfact.hpp index 6a42377901..da01c004e9 100644 --- a/components/esm/loadfact.hpp +++ b/components/esm/loadfact.hpp @@ -32,7 +32,7 @@ struct Faction { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Faction"; } + static std::string_view getRecordType() { return "Faction"; } unsigned int mRecordFlags; std::string mId, mName; diff --git a/components/esm/loadglob.hpp b/components/esm/loadglob.hpp index 9dd58e6c67..7fcbe4b8e6 100644 --- a/components/esm/loadglob.hpp +++ b/components/esm/loadglob.hpp @@ -19,7 +19,7 @@ struct Global { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Global"; } + static std::string_view getRecordType() { return "Global"; } unsigned int mRecordFlags; std::string mId; diff --git a/components/esm/loadgmst.hpp b/components/esm/loadgmst.hpp index 931ee286a4..a3981736f1 100644 --- a/components/esm/loadgmst.hpp +++ b/components/esm/loadgmst.hpp @@ -20,7 +20,7 @@ struct GameSetting { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "GameSetting"; } + static std::string_view getRecordType() { return "GameSetting"; } unsigned int mRecordFlags; std::string mId; diff --git a/components/esm/loadinfo.cpp b/components/esm/loadinfo.cpp index 15921249f4..6c54b0b9ab 100644 --- a/components/esm/loadinfo.cpp +++ b/components/esm/loadinfo.cpp @@ -23,7 +23,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'D','A','T','A'>::value: esm.getHT(mData, 12); diff --git a/components/esm/loadinfo.hpp b/components/esm/loadinfo.hpp index 2fbc782ec9..d68301c91b 100644 --- a/components/esm/loadinfo.hpp +++ b/components/esm/loadinfo.hpp @@ -22,7 +22,7 @@ struct DialInfo { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "DialInfo"; } + static std::string_view getRecordType() { return "DialInfo"; } enum Gender { diff --git a/components/esm/loadingr.cpp b/components/esm/loadingr.cpp index 26873a6eea..1aba000267 100644 --- a/components/esm/loadingr.cpp +++ b/components/esm/loadingr.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadingr.hpp b/components/esm/loadingr.hpp index 34e05da2c1..2ee572123d 100644 --- a/components/esm/loadingr.hpp +++ b/components/esm/loadingr.hpp @@ -17,7 +17,7 @@ struct Ingredient { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Ingredient"; } + static std::string_view getRecordType() { return "Ingredient"; } struct IRDTstruct { diff --git a/components/esm/loadland.cpp b/components/esm/loadland.cpp index 1fac79082d..e97ad6b759 100644 --- a/components/esm/loadland.cpp +++ b/components/esm/loadland.cpp @@ -15,7 +15,6 @@ namespace ESM : mFlags(0) , mX(0) , mY(0) - , mPlugin(0) , mDataTypes(0) , mLandData(nullptr) { @@ -40,14 +39,12 @@ namespace ESM { isDeleted = false; - mPlugin = esm.getIndex(); - bool hasLocation = false; bool isLoaded = false; while (!isLoaded && esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'I','N','T','V'>::value: esm.getSubHeader(); @@ -83,7 +80,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'V','N','M','L'>::value: esm.skipHSub(); @@ -172,7 +169,7 @@ namespace ESM { float height = mLandData->mHeights[int(row * vertMult) * ESM::Land::LAND_SIZE + int(col * vertMult)]; height /= height > 0 ? 128.f : 16.f; - height = std::min(max, std::max(min, height)); + height = std::clamp(height, min, max); wnam[row * LAND_GLOBAL_MAP_LOD_SIZE_SQRT + col] = static_cast(height); } } @@ -192,7 +189,7 @@ namespace ESM void Land::blank() { - mPlugin = 0; + setPlugin(0); std::fill(std::begin(mWnam), std::end(mWnam), 0); @@ -326,7 +323,7 @@ namespace ESM } Land::Land (const Land& land) - : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mPlugin (land.mPlugin), + : mFlags (land.mFlags), mX (land.mX), mY (land.mY), mContext (land.mContext), mDataTypes (land.mDataTypes), mLandData (land.mLandData ? new LandData (*land.mLandData) : nullptr) { @@ -345,7 +342,6 @@ namespace ESM std::swap (mFlags, land.mFlags); std::swap (mX, land.mX); std::swap (mY, land.mY); - std::swap (mPlugin, land.mPlugin); std::swap (mContext, land.mContext); std::swap (mDataTypes, land.mDataTypes); std::swap (mLandData, land.mLandData); diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 9cba41b160..610dd28fb8 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -21,7 +21,7 @@ struct Land { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Land"; } + static std::string_view getRecordType() { return "Land"; } Land(); ~Land(); @@ -29,7 +29,10 @@ struct Land int mFlags; // Only first four bits seem to be used, don't know what // they mean. int mX, mY; // Map coordinates. - int mPlugin; // Plugin index, used to reference the correct material palette. + + // Plugin index, used to reference the correct material palette. + int getPlugin() const { return mContext.index; } + void setPlugin(int index) { mContext.index = index; } // File context. This allows the ESM reader to be 'reset' to this // location later when we are ready to load the full data set. diff --git a/components/esm/loadlevlist.cpp b/components/esm/loadlevlist.cpp index 450bf06ecb..acf97f4259 100644 --- a/components/esm/loadlevlist.cpp +++ b/components/esm/loadlevlist.cpp @@ -16,7 +16,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadlevlist.hpp b/components/esm/loadlevlist.hpp index d3451b70ae..0ebd7a64cb 100644 --- a/components/esm/loadlevlist.hpp +++ b/components/esm/loadlevlist.hpp @@ -48,7 +48,7 @@ struct CreatureLevList: LevelledListBase { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "CreatureLevList"; } + static std::string_view getRecordType() { return "CreatureLevList"; } enum Flags { @@ -68,7 +68,7 @@ struct ItemLevList: LevelledListBase { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "ItemLevList"; } + static std::string_view getRecordType() { return "ItemLevList"; } enum Flags { diff --git a/components/esm/loadligh.cpp b/components/esm/loadligh.cpp index e53c82cc3f..32c0b16243 100644 --- a/components/esm/loadligh.cpp +++ b/components/esm/loadligh.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadligh.hpp b/components/esm/loadligh.hpp index 901fe6ec85..9bd608e4d9 100644 --- a/components/esm/loadligh.hpp +++ b/components/esm/loadligh.hpp @@ -18,7 +18,7 @@ struct Light { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Light"; } + static std::string_view getRecordType() { return "Light"; } enum Flags { diff --git a/components/esm/loadlock.cpp b/components/esm/loadlock.cpp index 88ce77e34e..bea5c86773 100644 --- a/components/esm/loadlock.cpp +++ b/components/esm/loadlock.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadlock.hpp b/components/esm/loadlock.hpp index 9cd40c0d4f..4f8a3575fc 100644 --- a/components/esm/loadlock.hpp +++ b/components/esm/loadlock.hpp @@ -13,7 +13,7 @@ struct Lockpick { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Lockpick"; } + static std::string_view getRecordType() { return "Lockpick"; } struct Data { diff --git a/components/esm/loadltex.cpp b/components/esm/loadltex.cpp index 6bdc40e5fc..f2a1e17d49 100644 --- a/components/esm/loadltex.cpp +++ b/components/esm/loadltex.cpp @@ -17,7 +17,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadltex.hpp b/components/esm/loadltex.hpp index e3e2582462..b2e937c01a 100644 --- a/components/esm/loadltex.hpp +++ b/components/esm/loadltex.hpp @@ -20,7 +20,7 @@ struct LandTexture { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "LandTexture"; } + static std::string_view getRecordType() { return "LandTexture"; } // mId is merely a user friendly name for the texture in the editor. std::string mId, mTexture; diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp index 4bc09920e9..badbbb2133 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm/loadmgef.cpp @@ -209,7 +209,7 @@ void MagicEffect::load(ESMReader &esm, bool &isDeleted) while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'I','T','E','X'>::value: mIcon = esm.getHString(); diff --git a/components/esm/loadmgef.hpp b/components/esm/loadmgef.hpp index 480478d81e..00cadf99c0 100644 --- a/components/esm/loadmgef.hpp +++ b/components/esm/loadmgef.hpp @@ -14,7 +14,7 @@ struct MagicEffect { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "MagicEffect"; } + static std::string_view getRecordType() { return "MagicEffect"; } unsigned int mRecordFlags; std::string mId; diff --git a/components/esm/loadmisc.cpp b/components/esm/loadmisc.cpp index 39d589eac8..a60012e74b 100644 --- a/components/esm/loadmisc.cpp +++ b/components/esm/loadmisc.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadmisc.hpp b/components/esm/loadmisc.hpp index 72aaa5de3b..a0f46349a8 100644 --- a/components/esm/loadmisc.hpp +++ b/components/esm/loadmisc.hpp @@ -18,7 +18,7 @@ struct Miscellaneous { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Miscellaneous"; } + static std::string_view getRecordType() { return "Miscellaneous"; } struct MCDTstruct { diff --git a/components/esm/loadnpc.cpp b/components/esm/loadnpc.cpp index e1fb9b5931..b86ea6f8bc 100644 --- a/components/esm/loadnpc.cpp +++ b/components/esm/loadnpc.cpp @@ -26,7 +26,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadnpc.hpp b/components/esm/loadnpc.hpp index ba9f415760..f0354cb603 100644 --- a/components/esm/loadnpc.hpp +++ b/components/esm/loadnpc.hpp @@ -24,7 +24,7 @@ struct NPC { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "NPC"; } + static std::string_view getRecordType() { return "NPC"; } // Services enum Services diff --git a/components/esm/loadpgrd.cpp b/components/esm/loadpgrd.cpp index 7bf60ca5fc..b10e3c453d 100644 --- a/components/esm/loadpgrd.cpp +++ b/components/esm/loadpgrd.cpp @@ -46,7 +46,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mCell = esm.getHString(); diff --git a/components/esm/loadpgrd.hpp b/components/esm/loadpgrd.hpp index 4e74c9a24d..02ce231fe3 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm/loadpgrd.hpp @@ -17,7 +17,7 @@ struct Pathgrid { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Pathgrid"; } + static std::string_view getRecordType() { return "Pathgrid"; } struct DATAstruct { diff --git a/components/esm/loadprob.cpp b/components/esm/loadprob.cpp index 8f03189686..edc6b89cd1 100644 --- a/components/esm/loadprob.cpp +++ b/components/esm/loadprob.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadprob.hpp b/components/esm/loadprob.hpp index 930e31a971..583aa24523 100644 --- a/components/esm/loadprob.hpp +++ b/components/esm/loadprob.hpp @@ -13,7 +13,7 @@ struct Probe { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Probe"; } + static std::string_view getRecordType() { return "Probe"; } struct Data { diff --git a/components/esm/loadrace.cpp b/components/esm/loadrace.cpp index 44dbde7742..35ad4421f1 100644 --- a/components/esm/loadrace.cpp +++ b/components/esm/loadrace.cpp @@ -30,7 +30,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadrace.hpp b/components/esm/loadrace.hpp index 50fa73ad74..ba42261e73 100644 --- a/components/esm/loadrace.hpp +++ b/components/esm/loadrace.hpp @@ -19,7 +19,7 @@ struct Race { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Race"; } + static std::string_view getRecordType() { return "Race"; } struct SkillBonus { diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index e39887a1b1..9cfdaaabc0 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -17,7 +17,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index 74f6b123ec..64991c9b3a 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -20,7 +20,7 @@ struct Region { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Region"; } + static std::string_view getRecordType() { return "Region"; } #pragma pack(push) #pragma pack(1) diff --git a/components/esm/loadrepa.cpp b/components/esm/loadrepa.cpp index ecb693c0c5..3fe62e740a 100644 --- a/components/esm/loadrepa.cpp +++ b/components/esm/loadrepa.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadrepa.hpp b/components/esm/loadrepa.hpp index 312c87b7b4..c3b2a076af 100644 --- a/components/esm/loadrepa.hpp +++ b/components/esm/loadrepa.hpp @@ -13,7 +13,7 @@ struct Repair { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Repair"; } + static std::string_view getRecordType() { return "Repair"; } struct Data { diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 8715c83dcb..76cb3c0149 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -91,7 +91,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'S','C','H','D'>::value: { diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index d518a048ff..f737698184 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -21,7 +21,7 @@ class Script public: static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Script"; } + static std::string_view getRecordType() { return "Script"; } struct SCHDstruct { diff --git a/components/esm/loadskil.cpp b/components/esm/loadskil.cpp index 9f58176f35..9236302dea 100644 --- a/components/esm/loadskil.cpp +++ b/components/esm/loadskil.cpp @@ -137,7 +137,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::FourCC<'I','N','D','X'>::value: esm.getHT(mIndex); diff --git a/components/esm/loadskil.hpp b/components/esm/loadskil.hpp index ae44a51045..404ef06692 100644 --- a/components/esm/loadskil.hpp +++ b/components/esm/loadskil.hpp @@ -20,7 +20,7 @@ struct Skill { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Skill"; } + static std::string_view getRecordType() { return "Skill"; } unsigned int mRecordFlags; std::string mId; diff --git a/components/esm/loadsndg.cpp b/components/esm/loadsndg.cpp index c439d0ca66..c1170b36c7 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm/loadsndg.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadsndg.hpp b/components/esm/loadsndg.hpp index 99aae06e0e..565a29b156 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm/loadsndg.hpp @@ -17,7 +17,7 @@ struct SoundGenerator { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "SoundGenerator"; } + static std::string_view getRecordType() { return "SoundGenerator"; } enum Type { diff --git a/components/esm/loadsoun.cpp b/components/esm/loadsoun.cpp index ed8fc519a6..9d58e19e99 100644 --- a/components/esm/loadsoun.cpp +++ b/components/esm/loadsoun.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadsoun.hpp b/components/esm/loadsoun.hpp index 14f1178650..4149a34b7d 100644 --- a/components/esm/loadsoun.hpp +++ b/components/esm/loadsoun.hpp @@ -18,7 +18,7 @@ struct Sound { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Sound"; } + static std::string_view getRecordType() { return "Sound"; } SOUNstruct mData; unsigned int mRecordFlags; diff --git a/components/esm/loadspel.cpp b/components/esm/loadspel.cpp index 5983cdcdf5..cc26024426 100644 --- a/components/esm/loadspel.cpp +++ b/components/esm/loadspel.cpp @@ -20,7 +20,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadspel.hpp b/components/esm/loadspel.hpp index ef74c2c312..6a2d02d286 100644 --- a/components/esm/loadspel.hpp +++ b/components/esm/loadspel.hpp @@ -15,7 +15,7 @@ struct Spell { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Spell"; } + static std::string_view getRecordType() { return "Spell"; } enum SpellType { diff --git a/components/esm/loadsscr.cpp b/components/esm/loadsscr.cpp index 9e060ab1a7..723fb3bf10 100644 --- a/components/esm/loadsscr.cpp +++ b/components/esm/loadsscr.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadsscr.hpp b/components/esm/loadsscr.hpp index 3e84027076..277c4eb731 100644 --- a/components/esm/loadsscr.hpp +++ b/components/esm/loadsscr.hpp @@ -21,7 +21,7 @@ struct StartScript { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "StartScript"; } + static std::string_view getRecordType() { return "StartScript"; } std::string mData; unsigned int mRecordFlags; diff --git a/components/esm/loadstat.cpp b/components/esm/loadstat.cpp index 1073ab48e2..e0c9eb2c7e 100644 --- a/components/esm/loadstat.cpp +++ b/components/esm/loadstat.cpp @@ -19,7 +19,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadstat.hpp b/components/esm/loadstat.hpp index 038fb9f52b..26d8fda3a8 100644 --- a/components/esm/loadstat.hpp +++ b/components/esm/loadstat.hpp @@ -24,7 +24,7 @@ struct Static { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Static"; } + static std::string_view getRecordType() { return "Static"; } unsigned int mRecordFlags; std::string mId, mModel; diff --git a/components/esm/loadweap.cpp b/components/esm/loadweap.cpp index 78b9bb4073..08c5a3b641 100644 --- a/components/esm/loadweap.cpp +++ b/components/esm/loadweap.cpp @@ -18,7 +18,7 @@ namespace ESM while (esm.hasMoreSubs()) { esm.getSubName(); - switch (esm.retSubName().intval) + switch (esm.retSubName().toInt()) { case ESM::SREC_NAME: mId = esm.getHString(); diff --git a/components/esm/loadweap.hpp b/components/esm/loadweap.hpp index 8d1591854b..98ba57b8b8 100644 --- a/components/esm/loadweap.hpp +++ b/components/esm/loadweap.hpp @@ -19,7 +19,7 @@ struct Weapon { static unsigned int sRecordId; /// Return a string descriptor for this record type. Currently used for debugging / error logs only. - static std::string getRecordType() { return "Weapon"; } + static std::string_view getRecordType() { return "Weapon"; } enum Type { diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index 1dd45ab2b1..c831cbbbfc 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -5,13 +5,15 @@ // List of all records, that are related to Lua. // -// Record: -// LUAM - MWLua::LuaManager +// Records: +// LUAL - LuaScriptsCfg - list of all scripts (in content files) +// LUAM - MWLua::LuaManager (in saves) // // Subrecords: +// LUAF - LuaScriptCfg::mFlags // LUAW - Start of MWLua::WorldView data // LUAE - Start of MWLua::LocalEvent or MWLua::GlobalEvent (eventName) -// LUAS - Start LuaUtil::ScriptsContainer data (scriptName) +// LUAS - VFS path to a Lua script // LUAD - Serialized Lua variable // LUAT - MWLua::ScriptsContainer::Timer // LUAC - Name of a timer callback (string) @@ -32,11 +34,33 @@ std::string ESM::loadLuaBinaryData(ESMReader& esm) { esm.getSubHeader(); data.resize(esm.getSubSize()); - esm.getExact(data.data(), data.size()); + esm.getExact(data.data(), static_cast(data.size())); } return data; } +void ESM::LuaScriptsCfg::load(ESMReader& esm) +{ + while (esm.isNextSub("LUAS")) + { + std::string name = esm.getHString(); + uint64_t flags; + esm.getHNT(flags, "LUAF"); + std::string data = loadLuaBinaryData(esm); + mScripts.push_back({std::move(name), std::move(data), flags}); + } +} + +void ESM::LuaScriptsCfg::save(ESMWriter& esm) const +{ + for (const LuaScriptCfg& script : mScripts) + { + esm.writeHNString("LUAS", script.mScriptPath); + esm.writeHNT("LUAF", script.mFlags); + saveLuaBinaryData(esm, script.mInitializationData); + } +} + void ESM::LuaScripts::load(ESMReader& esm) { while (esm.isNextSub("LUAS")) @@ -63,8 +87,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const for (const LuaScript& script : mScripts) { esm.writeHNString("LUAS", script.mScriptPath); - if (!script.mData.empty()) - saveLuaBinaryData(esm, script.mData); + saveLuaBinaryData(esm, script.mData); for (const LuaTimer& timer : script.mTimers) { esm.startSubRecord("LUAT"); diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index f268f41536..e6f7113c16 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -9,7 +9,44 @@ namespace ESM class ESMReader; class ESMWriter; - // Storage structure for LuaUtil::ScriptsContainer. This is not a top-level record. + // LuaScriptCfg, LuaScriptsCfg are used in content files. + + struct LuaScriptCfg + { + using Flags = uint64_t; + static constexpr Flags sGlobal = 1ull << 0; + static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script + static constexpr Flags sPlayer = 1ull << 2; // auto attach to players + // auto attach for other classes: + static constexpr Flags sActivator = 1ull << 3; + static constexpr Flags sArmor = 1ull << 4; + static constexpr Flags sBook = 1ull << 5; + static constexpr Flags sClothing = 1ull << 6; + static constexpr Flags sContainer = 1ull << 7; + static constexpr Flags sCreature = 1ull << 8; + static constexpr Flags sDoor = 1ull << 9; + static constexpr Flags sIngredient = 1ull << 10; + static constexpr Flags sLight = 1ull << 11; + static constexpr Flags sMiscItem = 1ull << 12; + static constexpr Flags sNPC = 1ull << 13; + static constexpr Flags sPotion = 1ull << 14; + static constexpr Flags sWeapon = 1ull << 15; + + std::string mScriptPath; + std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'. + Flags mFlags; // bitwise OR of Flags. + }; + + struct LuaScriptsCfg + { + std::vector mScripts; + + void load(ESMReader &esm); + void save(ESMWriter &esm) const; + }; + + // LuaTimer, LuaScript, LuaScripts are used in saved game files. + // Storage structure for LuaUtil::ScriptsContainer. These are not top-level records. // Used either for global scripts or for local scripts on a specific object. struct LuaTimer @@ -37,11 +74,11 @@ namespace ESM { std::vector mScripts; - void load (ESMReader &esm); - void save (ESMWriter &esm) const; + void load(ESMReader &esm); + void save(ESMWriter &esm) const; }; - // Saves binary string `data` (can contain '\0') as record LUAD. + // Saves binary string `data` (can contain '\0') as LUAD record. void saveLuaBinaryData(ESM::ESMWriter& esm, const std::string& data); // Loads LUAD as binary string. If next subrecord is not LUAD, then returns an empty string. diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 8a98a63419..4ce0876bb8 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -4,7 +4,7 @@ #include "esmwriter.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 17; +int ESM::SavedGame::sCurrentFormat = 18; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/esm/transport.cpp b/components/esm/transport.cpp index 11676ea723..9d40debf73 100644 --- a/components/esm/transport.cpp +++ b/components/esm/transport.cpp @@ -10,13 +10,13 @@ namespace ESM void Transport::add(ESMReader &esm) { - if (esm.retSubName().intval == ESM::FourCC<'D','O','D','T'>::value) + if (esm.retSubName().toInt() == ESM::FourCC<'D','O','D','T'>::value) { Dest dodt; esm.getHExact(&dodt.mPos, 24); mList.push_back(dodt); } - else if (esm.retSubName().intval == ESM::FourCC<'D','N','A','M'>::value) + else if (esm.retSubName().toInt() == ESM::FourCC<'D','N','A','M'>::value) { const std::string name = esm.getHString(); if (mList.empty()) diff --git a/components/esmloader/esmdata.cpp b/components/esmloader/esmdata.cpp new file mode 100644 index 0000000000..bf2a8675d2 --- /dev/null +++ b/components/esmloader/esmdata.cpp @@ -0,0 +1,77 @@ +#include "esmdata.hpp" +#include "lessbyid.hpp" +#include "record.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace EsmLoader +{ + namespace + { + template + auto returnAs(F&& f) + { + using Result = decltype(std::forward(f)(ESM::Static {})); + if constexpr (!std::is_same_v) + return Result {}; + } + + template + auto withStatic(std::string_view refId, const std::vector& values, F&& f) + { + const auto it = std::lower_bound(values.begin(), values.end(), refId, LessById {}); + + if (it == values.end() || it->mId != refId) + return returnAs(std::forward(f)); + + return std::forward(f)(*it); + } + + template + auto withStatic(std::string_view refId, ESM::RecNameInts type, const EsmData& content, F&& f) + { + switch (type) + { + case ESM::REC_ACTI: return withStatic(refId, content.mActivators, std::forward(f)); + case ESM::REC_CONT: return withStatic(refId, content.mContainers, std::forward(f)); + case ESM::REC_DOOR: return withStatic(refId, content.mDoors, std::forward(f)); + case ESM::REC_STAT: return withStatic(refId, content.mStatics, std::forward(f)); + default: break; + } + + return returnAs(std::forward(f)); + } + } + + EsmData::~EsmData() {} + + std::string_view getModel(const EsmData& content, std::string_view refId, ESM::RecNameInts type) + { + return withStatic(refId, type, content, [] (const auto& v) { return std::string_view(v.mModel); }); + } + + ESM::Variant getGameSetting(const std::vector& records, std::string_view id) + { + const std::string lower = Misc::StringUtils::lowerCase(id); + auto it = std::lower_bound(records.begin(), records.end(), lower, LessById {}); + if (it == records.end() || it->mId != lower) + throw std::runtime_error("Game settings \"" + std::string(id) + "\" is not found"); + return it->mValue; + } +} diff --git a/components/esmloader/esmdata.hpp b/components/esmloader/esmdata.hpp new file mode 100644 index 0000000000..afdcc1748d --- /dev/null +++ b/components/esmloader/esmdata.hpp @@ -0,0 +1,52 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_ESMDATA_H +#define OPENMW_COMPONENTS_ESMLOADER_ESMDATA_H + +#include + +#include +#include + +namespace ESM +{ + struct Activator; + struct Cell; + struct Container; + struct Door; + struct GameSetting; + struct Land; + struct Static; + class Variant; +} + +namespace EsmLoader +{ + struct RefIdWithType + { + std::string_view mId; + ESM::RecNameInts mType; + }; + + struct EsmData + { + std::vector mActivators; + std::vector mCells; + std::vector mContainers; + std::vector mDoors; + std::vector mGameSettings; + std::vector mLands; + std::vector mStatics; + std::vector mRefIdTypes; + + EsmData() = default; + EsmData(const EsmData&) = delete; + EsmData(EsmData&&) = default; + + ~EsmData(); + }; + + std::string_view getModel(const EsmData& content, std::string_view refId, ESM::RecNameInts type); + + ESM::Variant getGameSetting(const std::vector& records, std::string_view id); +} + +#endif diff --git a/components/esmloader/lessbyid.hpp b/components/esmloader/lessbyid.hpp new file mode 100644 index 0000000000..da835c9e39 --- /dev/null +++ b/components/esmloader/lessbyid.hpp @@ -0,0 +1,24 @@ +#ifndef OPENMW_COMPONENTS_CONTENT_LESSBYID_H +#define OPENMW_COMPONENTS_CONTENT_LESSBYID_H + +#include + +namespace EsmLoader +{ + struct LessById + { + template + bool operator()(const T& lhs, const T& rhs) const + { + return lhs.mId < rhs.mId; + } + + template + bool operator()(const T& lhs, std::string_view rhs) const + { + return lhs.mId < rhs; + } + }; +} + +#endif diff --git a/components/esmloader/load.cpp b/components/esmloader/load.cpp new file mode 100644 index 0000000000..842cc8003a --- /dev/null +++ b/components/esmloader/load.cpp @@ -0,0 +1,352 @@ +#include "load.hpp" +#include "esmdata.hpp" +#include "lessbyid.hpp" +#include "record.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EsmLoader +{ + namespace + { + struct GetKey + { + template + decltype(auto) operator()(const T& v) const + { + return (v.mId); + } + + const ESM::CellId& operator()(const ESM::Cell& v) const + { + return v.mCellId; + } + + std::pair operator()(const ESM::Land& v) const + { + return std::pair(v.mX, v.mY); + } + + template + decltype(auto) operator()(const Record& v) const + { + return (*this)(v.mValue); + } + }; + + struct CellRecords + { + Records mValues; + std::map mByName; + std::map, std::size_t> mByPosition; + }; + + template > + struct HasId : std::false_type {}; + + template + struct HasId> : std::true_type {}; + + template + constexpr bool hasId = HasId::value; + + template + auto loadRecord(ESM::ESMReader& reader, Records& records) + -> std::enable_if_t> + { + T record; + bool deleted = false; + record.load(reader, deleted); + Misc::StringUtils::lowerCaseInPlace(record.mId); + if (Misc::ResourceHelpers::isHiddenMarker(record.mId)) + return; + records.emplace_back(deleted, std::move(record)); + } + + template + auto loadRecord(ESM::ESMReader& reader, Records& records) + -> std::enable_if_t> + { + T record; + bool deleted = false; + record.load(reader, deleted); + records.emplace_back(deleted, std::move(record)); + } + + void loadRecord(ESM::ESMReader& reader, CellRecords& records) + { + ESM::Cell record; + bool deleted = false; + record.loadNameAndData(reader, deleted); + Misc::StringUtils::lowerCaseInPlace(record.mName); + + if ((record.mData.mFlags & ESM::Cell::Interior) != 0) + { + const auto it = records.mByName.find(record.mName); + if (it == records.mByName.end()) + { + record.loadCell(reader, true); + records.mByName.emplace_hint(it, record.mName, records.mValues.size()); + records.mValues.emplace_back(deleted, std::move(record)); + } + else + { + Record& old = records.mValues[it->second]; + old.mValue.mData = record.mData; + old.mValue.loadCell(reader, true); + } + } + else + { + const std::pair position(record.mData.mX, record.mData.mY); + const auto it = records.mByPosition.find(position); + if (it == records.mByPosition.end()) + { + record.loadCell(reader, true); + records.mByPosition.emplace_hint(it, position, records.mValues.size()); + records.mValues.emplace_back(deleted, std::move(record)); + } + else + { + Record& old = records.mValues[it->second]; + old.mValue.mData = record.mData; + old.mValue.loadCell(reader, true); + } + } + } + + struct ShallowContent + { + Records mActivators; + CellRecords mCells; + Records mContainers; + Records mDoors; + Records mGameSettings; + Records mLands; + Records mStatics; + }; + + void loadRecord(const Query& query, const ESM::NAME& name, ESM::ESMReader& reader, ShallowContent& content) + { + switch (name.toInt()) + { + case ESM::REC_ACTI: + if (query.mLoadActivators) + return loadRecord(reader, content.mActivators); + break; + case ESM::REC_CELL: + if (query.mLoadCells) + return loadRecord(reader, content.mCells); + break; + case ESM::REC_CONT: + if (query.mLoadContainers) + return loadRecord(reader, content.mContainers); + break; + case ESM::REC_DOOR: + if (query.mLoadDoors) + return loadRecord(reader, content.mDoors); + break; + case ESM::REC_GMST: + if (query.mLoadGameSettings) + return loadRecord(reader, content.mGameSettings); + break; + case ESM::REC_LAND: + if (query.mLoadLands) + return loadRecord(reader, content.mLands); + break; + case ESM::REC_STAT: + if (query.mLoadStatics) + return loadRecord(reader, content.mStatics); + break; + } + + reader.skipRecord(); + } + + ESM::ESMReader loadEsm(const Query& query, ESM::ESMReader& reader, ShallowContent& content) + { + Log(Debug::Info) << "Loading ESM file " << reader.getName(); + + while (reader.hasMoreRecs()) + { + const ESM::NAME recName = reader.getRecName(); + reader.getRecHeader(); + loadRecord(query, recName, reader, content); + } + + return reader; + } + + ShallowContent shallowLoad(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, std::vector& readers, + ToUTF8::Utf8Encoder* encoder) + { + ShallowContent result; + + const std::set supportedFormats { + ".esm", + ".esp", + ".omwgame", + ".omwaddon", + ".project", + }; + + for (std::size_t i = 0; i < contentFiles.size(); ++i) + { + const std::string &file = contentFiles[i]; + const std::string extension = Misc::StringUtils::lowerCase(boost::filesystem::path(file).extension().string()); + + if (supportedFormats.find(extension) == supportedFormats.end()) + { + Log(Debug::Warning) << "Skipping unsupported content file: " << file; + continue; + } + + const Files::MultiDirCollection& collection = fileCollections.getCollection(extension); + + ESM::ESMReader& reader = readers[i]; + reader.setEncoder(encoder); + reader.setIndex(static_cast(i)); + reader.open(collection.getPath(file).string()); + if (query.mLoadCells) + reader.resolveParentFileIndices(readers); + + loadEsm(query, readers[i], result); + } + + return result; + } + + struct WithType + { + ESM::RecNameInts mType; + + template + RefIdWithType operator()(const T& v) const { return {v.mId, mType}; } + }; + + template + void addRefIdsTypes(const std::vector& values, std::vector& refIdsTypes) + { + std::transform(values.begin(), values.end(), std::back_inserter(refIdsTypes), + WithType {static_cast(T::sRecordId)}); + } + + void addRefIdsTypes(EsmData& content) + { + content.mRefIdTypes.reserve( + content.mActivators.size() + + content.mContainers.size() + + content.mDoors.size() + + content.mStatics.size() + ); + + addRefIdsTypes(content.mActivators, content.mRefIdTypes); + addRefIdsTypes(content.mContainers, content.mRefIdTypes); + addRefIdsTypes(content.mDoors, content.mRefIdTypes); + addRefIdsTypes(content.mStatics, content.mRefIdTypes); + + std::sort(content.mRefIdTypes.begin(), content.mRefIdTypes.end(), LessById {}); + } + + std::vector prepareCellRecords(Records& records) + { + std::vector result; + for (Record& v : records) + if (!v.mDeleted) + result.emplace_back(std::move(v.mValue)); + return result; + } + } + + EsmData loadEsmData(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, std::vector& readers, ToUTF8::Utf8Encoder* encoder) + { + Log(Debug::Info) << "Loading ESM data..."; + + ShallowContent content = shallowLoad(query, contentFiles, fileCollections, readers, encoder); + + std::ostringstream loaded; + + if (query.mLoadActivators) + loaded << ' ' << content.mActivators.size() << " activators,"; + if (query.mLoadCells) + loaded << ' ' << content.mCells.mValues.size() << " cells,"; + if (query.mLoadContainers) + loaded << ' ' << content.mContainers.size() << " containers,"; + if (query.mLoadDoors) + loaded << ' ' << content.mDoors.size() << " doors,"; + if (query.mLoadGameSettings) + loaded << ' ' << content.mGameSettings.size() << " game settings,"; + if (query.mLoadLands) + loaded << ' ' << content.mLands.size() << " lands,"; + if (query.mLoadStatics) + loaded << ' ' << content.mStatics.size() << " statics,"; + + Log(Debug::Info) << "Loaded" << loaded.str(); + + EsmData result; + + if (query.mLoadActivators) + result.mActivators = prepareRecords(content.mActivators, GetKey {}); + if (query.mLoadCells) + result.mCells = prepareCellRecords(content.mCells.mValues); + if (query.mLoadContainers) + result.mContainers = prepareRecords(content.mContainers, GetKey {}); + if (query.mLoadDoors) + result.mDoors = prepareRecords(content.mDoors, GetKey {}); + if (query.mLoadGameSettings) + result.mGameSettings = prepareRecords(content.mGameSettings, GetKey {}); + if (query.mLoadLands) + result.mLands = prepareRecords(content.mLands, GetKey {}); + if (query.mLoadStatics) + result.mStatics = prepareRecords(content.mStatics, GetKey {}); + + addRefIdsTypes(result); + + std::ostringstream prepared; + + if (query.mLoadActivators) + prepared << ' ' << result.mActivators.size() << " unique activators,"; + if (query.mLoadCells) + prepared << ' ' << result.mCells.size() << " unique cells,"; + if (query.mLoadContainers) + prepared << ' ' << result.mContainers.size() << " unique containers,"; + if (query.mLoadDoors) + prepared << ' ' << result.mDoors.size() << " unique doors,"; + if (query.mLoadGameSettings) + prepared << ' ' << result.mGameSettings.size() << " unique game settings,"; + if (query.mLoadLands) + prepared << ' ' << result.mLands.size() << " unique lands,"; + if (query.mLoadStatics) + prepared << ' ' << result.mStatics.size() << " unique statics,"; + + Log(Debug::Info) << "Prepared" << prepared.str(); + + return result; + } +} diff --git a/components/esmloader/load.hpp b/components/esmloader/load.hpp new file mode 100644 index 0000000000..39d6f48b81 --- /dev/null +++ b/components/esmloader/load.hpp @@ -0,0 +1,39 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_LOAD_H +#define OPENMW_COMPONENTS_ESMLOADER_LOAD_H + +#include + +#include +#include + +namespace ToUTF8 +{ + class Utf8Encoder; +} + +namespace Files +{ + class Collections; +} + +namespace EsmLoader +{ + struct EsmData; + + struct Query + { + bool mLoadActivators = false; + bool mLoadCells = false; + bool mLoadContainers = false; + bool mLoadDoors = false; + bool mLoadGameSettings = false; + bool mLoadLands = false; + bool mLoadStatics = false; + }; + + EsmData loadEsmData(const Query& query, const std::vector& contentFiles, + const Files::Collections& fileCollections, std::vector& readers, + ToUTF8::Utf8Encoder* encoder); +} + +#endif diff --git a/components/esmloader/record.hpp b/components/esmloader/record.hpp new file mode 100644 index 0000000000..c076ee72c6 --- /dev/null +++ b/components/esmloader/record.hpp @@ -0,0 +1,44 @@ +#ifndef OPENMW_COMPONENTS_ESMLOADER_RECORD_H +#define OPENMW_COMPONENTS_ESMLOADER_RECORD_H + +#include + +#include +#include +#include + +namespace EsmLoader +{ + template + struct Record + { + bool mDeleted; + T mValue; + + template + explicit Record(bool deleted, Args&& ... args) + : mDeleted(deleted) + , mValue(std::forward(args) ...) + {} + }; + + template + using Records = std::vector>; + + template + inline std::vector prepareRecords(Records& records, const GetKey& getKey) + { + const auto greaterByKey = [&] (const auto& l, const auto& r) { return getKey(r) < getKey(l); }; + const auto equalByKey = [&] (const auto& l, const auto& r) { return getKey(l) == getKey(r); }; + std::stable_sort(records.begin(), records.end(), greaterByKey); + records.erase(std::unique(records.begin(), records.end(), equalByKey), records.end()); + std::reverse(records.begin(), records.end()); + std::vector result; + for (Record& v : records) + if (!v.mDeleted) + result.emplace_back(std::move(v.mValue)); + return result; + } +} + +#endif diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 68e71574ee..107255a7af 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -36,11 +36,7 @@ namespace ESMTerrain return nullptr; return &mData; } - - inline int getPlugin() const - { - return mLand->mPlugin; - } + inline int getPlugin() const { return mLand->getPlugin(); } private: const ESM::Land* mLand; diff --git a/components/files/hash.cpp b/components/files/hash.cpp new file mode 100644 index 0000000000..079a169ae5 --- /dev/null +++ b/components/files/hash.cpp @@ -0,0 +1,41 @@ +#include "hash.hpp" + +#include + +#include +#include +#include +#include + +namespace Files +{ + std::array getHash(const std::string& fileName, std::istream& stream) + { + std::array hash {0, 0}; + try + { + const auto start = stream.tellg(); + const auto exceptions = stream.exceptions(); + stream.exceptions(std::ios_base::badbit); + while (stream) + { + std::array value; + stream.read(value.data(), value.size()); + const std::streamsize read = stream.gcount(); + if (read == 0) + break; + std::array blockHash {0, 0}; + MurmurHash3_x64_128(value.data(), static_cast(read), hash.data(), blockHash.data()); + hash = blockHash; + } + stream.exceptions(exceptions); + stream.clear(); + stream.seekg(start); + } + catch (const std::exception& e) + { + throw std::runtime_error("Error while reading \"" + fileName + "\" to get hash: " + std::string(e.what())); + } + return hash; + } +} diff --git a/components/files/hash.hpp b/components/files/hash.hpp new file mode 100644 index 0000000000..13d56d5824 --- /dev/null +++ b/components/files/hash.hpp @@ -0,0 +1,14 @@ +#ifndef COMPONENTS_FILES_HASH_H +#define COMPONENTS_FILES_HASH_H + +#include +#include +#include +#include + +namespace Files +{ + std::array getHash(const std::string& fileName, std::istream& stream); +} + +#endif diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index da43cc38ec..76f554bec7 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -145,7 +145,7 @@ namespace Gui FontLoader::FontLoader(ToUTF8::FromType encoding, const VFS::Manager* vfs, const std::string& userDataPath, float scalingFactor) : mVFS(vfs) , mUserDataPath(userDataPath) - , mFontHeight(16) + , mFontHeight(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)) , mScalingFactor(scalingFactor) { if (encoding == ToUTF8::WINDOWS_1252) @@ -153,9 +153,6 @@ namespace Gui else mEncoding = encoding; - int fontSize = Settings::Manager::getInt("font size", "GUI"); - mFontHeight = std::min(std::max(12, fontSize), 20); - MyGUI::ResourceManager::getInstance().unregisterLoadXmlDelegate("Resource"); MyGUI::ResourceManager::getInstance().registerLoadXmlDelegate("Resource") = MyGUI::newDelegate(this, &FontLoader::loadFontFromXml); } @@ -549,7 +546,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)) * mScalingFactor; + resolution = std::clamp(resolution, 48, 960) * mScalingFactor; MyGUI::xml::ElementPtr resolutionNode = resourceNode->createChild("Property"); resolutionNode->addAttribute("key", "Resolution"); @@ -591,7 +588,7 @@ namespace Gui // setup separate fonts with different Resolution to fit these windows. // These fonts have an internal prefix. int resolution = Settings::Manager::getInt("ttf resolution", "GUI"); - resolution = std::min(960, std::max(48, resolution)); + resolution = std::clamp(resolution, 48, 960); float currentX = Settings::Manager::getInt("resolution x", "Video"); float currentY = Settings::Manager::getInt("resolution y", "Video"); diff --git a/components/interpreter/defines.cpp b/components/interpreter/defines.cpp index 48d2ceaa45..164e7b2126 100644 --- a/components/interpreter/defines.cpp +++ b/components/interpreter/defines.cpp @@ -172,8 +172,7 @@ namespace Interpreter{ for(unsigned int j = 0; j < globals.size(); j++){ if(globals[j].length() > temp.length()){ // Just in case there's a global with a huuuge name - temp = text.substr(i+1, globals[j].length()); - transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + temp = Misc::StringUtils::lowerCase(text.substr(i+1, globals[j].length())); } found = check(temp, globals[j], &i, &start); diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp new file mode 100644 index 0000000000..172d958029 --- /dev/null +++ b/components/lua/configuration.cpp @@ -0,0 +1,170 @@ +#include "configuration.hpp" + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + + namespace + { + const std::map> flagsByName{ + {"GLOBAL", ESM::LuaScriptCfg::sGlobal}, + {"CUSTOM", ESM::LuaScriptCfg::sCustom}, + {"PLAYER", ESM::LuaScriptCfg::sPlayer}, + {"ACTIVATOR", ESM::LuaScriptCfg::sActivator}, + {"ARMOR", ESM::LuaScriptCfg::sArmor}, + {"BOOK", ESM::LuaScriptCfg::sBook}, + {"CLOTHING", ESM::LuaScriptCfg::sClothing}, + {"CONTAINER", ESM::LuaScriptCfg::sContainer}, + {"CREATURE", ESM::LuaScriptCfg::sCreature}, + {"DOOR", ESM::LuaScriptCfg::sDoor}, + {"INGREDIENT", ESM::LuaScriptCfg::sIngredient}, + {"LIGHT", ESM::LuaScriptCfg::sLight}, + {"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem}, + {"NPC", ESM::LuaScriptCfg::sNPC}, + {"POTION", ESM::LuaScriptCfg::sPotion}, + {"WEAPON", ESM::LuaScriptCfg::sWeapon}, + }; + + bool isSpace(char c) + { + return std::isspace(static_cast(c)); + } + } + + const std::vector ScriptsConfiguration::sEmpty; + + void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg) + { + mScripts.clear(); + mScriptsByFlag.clear(); + mPathToIndex.clear(); + + // Find duplicates; only the last occurrence will be used. + // Search for duplicates is case insensitive. + std::vector skip(cfg.mScripts.size(), false); + for (int i = cfg.mScripts.size() - 1; i >= 0; --i) + { + auto [_, inserted] = mPathToIndex.insert_or_assign( + Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1); + if (!inserted || cfg.mScripts[i].mFlags == 0) + skip[i] = true; + } + mPathToIndex.clear(); + int index = 0; + for (size_t i = 0; i < cfg.mScripts.size(); ++i) + { + if (skip[i]) + continue; + ESM::LuaScriptCfg& s = cfg.mScripts[i]; + mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive. + ESM::LuaScriptCfg::Flags flags = s.mFlags; + ESM::LuaScriptCfg::Flags flag = 1; + while (flags != 0) + { + if (flags & flag) + mScriptsByFlag[flag].push_back(index); + flags &= ~flag; + flag = flag << 1; + } + mScripts.push_back(std::move(s)); + index++; + } + } + + std::optional ScriptsConfiguration::findId(std::string_view path) const + { + auto it = mPathToIndex.find(path); + if (it != mPathToIndex.end()) + return it->second; + else + return std::nullopt; + } + + const std::vector& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const + { + assert(std::bitset<64>(type).count() <= 1); + auto it = mScriptsByFlag.find(type); + if (it != mScriptsByFlag.end()) + return it->second; + else + return sEmpty; + } + + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data) + { + while (!data.empty()) + { + // Get next line + std::string_view line = data.substr(0, data.find('\n')); + data = data.substr(std::min(line.size() + 1, data.size())); + if (!line.empty() && line.back() == '\r') + line = line.substr(0, line.size() - 1); + + while (!line.empty() && isSpace(line[0])) + line = line.substr(1); + if (line.empty() || line[0] == '#') // Skip empty lines and comments + continue; + while (!line.empty() && isSpace(line.back())) + line = line.substr(0, line.size() - 1); + + if (!Misc::StringUtils::ciEndsWith(line, ".lua")) + throw std::runtime_error(Misc::StringUtils::format( + "Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300)))); + + // Split flags and script path + size_t semicolonPos = line.find(':'); + if (semicolonPos == std::string::npos) + throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line))); + std::string_view flagsStr = line.substr(0, semicolonPos); + std::string_view scriptPath = line.substr(semicolonPos + 1); + while (isSpace(scriptPath[0])) + scriptPath = scriptPath.substr(1); + + // Parse flags + ESM::LuaScriptCfg::Flags flags = 0; + size_t flagsPos = 0; + while (true) + { + while (flagsPos < flagsStr.size() && (isSpace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ',')) + flagsPos++; + size_t startPos = flagsPos; + while (flagsPos < flagsStr.size() && !isSpace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',') + flagsPos++; + if (startPos == flagsPos) + break; + std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos); + auto it = flagsByName.find(flagName); + if (it != flagsByName.end()) + flags |= it->second; + else + throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s", + std::string(flagName), std::string(line))); + } + if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal) + throw std::runtime_error("Global script can not have local flags"); + + cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags}); + } + } + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script) + { + std::stringstream ss; + for (const auto& [flagName, flag] : flagsByName) + { + if (script.mFlags & flag) + ss << flagName << " "; + } + ss << ": " << script.mScriptPath; + if (!script.mInitializationData.empty()) + ss << " (with data, " << script.mInitializationData.size() << " bytes)"; + return ss.str(); + } + +} diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp new file mode 100644 index 0000000000..32eddf399c --- /dev/null +++ b/components/lua/configuration.hpp @@ -0,0 +1,37 @@ +#ifndef COMPONENTS_LUA_CONFIGURATION_H +#define COMPONENTS_LUA_CONFIGURATION_H + +#include +#include + +#include + +namespace LuaUtil +{ + + class ScriptsConfiguration + { + public: + void init(ESM::LuaScriptsCfg); + + size_t size() const { return mScripts.size(); } + const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; } + + std::optional findId(std::string_view path) const; + const std::vector& getListByFlag(ESM::LuaScriptCfg::Flags type) const; + + private: + std::vector mScripts; + std::map> mPathToIndex; + std::map> mScriptsByFlag; + static const std::vector sEmpty; + }; + + // Parse ESM::LuaScriptsCfg from text and add to `cfg`. + void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data); + + std::string scriptCfgToString(const ESM::LuaScriptCfg& script); + +} + +#endif // COMPONENTS_LUA_CONFIGURATION_H diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 8e4719dba4..e78f7bed06 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -22,7 +22,7 @@ namespace LuaUtil "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "getmetatable", "setmetatable"}; static const std::string safePackages[] = {"coroutine", "math", "string", "table"}; - LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) + LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) { mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table, sol::lib::debug); @@ -95,12 +95,11 @@ namespace LuaUtil return res; } - void LuaState::addCommonPackage(const std::string& packageName, const sol::object& package) + void LuaState::addCommonPackage(std::string packageName, sol::object package) { - if (package.is()) - mCommonPackages[packageName] = package; - else - mCommonPackages[packageName] = makeReadOnly(package); + if (!package.is()) + package = makeReadOnly(std::move(package)); + mCommonPackages.emplace(std::move(packageName), std::move(package)); } sol::protected_function_result LuaState::runInNewSandbox( @@ -148,7 +147,7 @@ namespace LuaUtil return std::move(res); } - sol::protected_function LuaState::loadScript(const std::string& path) + sol::function LuaState::loadScript(const std::string& path) { auto iter = mCompiledScripts.find(path); if (iter != mCompiledScripts.end()) diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 8982b49b36..71cdb4f75d 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -7,6 +7,8 @@ #include +#include "configuration.hpp" + namespace LuaUtil { @@ -22,25 +24,39 @@ namespace LuaUtil // - Access to common read-only resources from different sandboxes; // - Replace standard `require` with a safe version that allows to search // Lua libraries (only source, no dll's) in the virtual filesystem; - // - Make `print` to add the script name to the every message and - // write to Log rather than directly to stdout; + // - Make `print` to add the script name to every message and + // write to the Log rather than directly to stdout; class LuaState { public: - explicit LuaState(const VFS::Manager* vfs); + explicit LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf); ~LuaState(); // Returns underlying sol::state. sol::state& sol() { return mLua; } + // Can be used by a C++ function that is called from Lua to get the Lua traceback. + // Makes no sense if called not from Lua code. + // Note: It is a slow function, should be used for debug purposes only. + std::string debugTraceback() { return mLua["debug"]["traceback"]().get(); } + // A shortcut to create a new Lua table. sol::table newTable() { return sol::table(mLua, sol::create); } + template + sol::table tableFromPairs(std::initializer_list> list) + { + sol::table res(mLua, sol::create); + for (const auto& [k, v] : list) + res[k] = v; + return res; + } + // Registers a package that will be available from every sandbox via `require(name)`. // The package can be either a sol::table with an API or a sol::function. If it is a function, // it will be evaluated (once per sandbox) the first time when requested. If the package // is a table, then `makeReadOnly` is applied to it automatically (but not to other tables it contains). - void addCommonPackage(const std::string& packageName, const sol::object& package); + void addCommonPackage(std::string packageName, sol::object package); // Creates a new sandbox, runs a script, and returns the result // (the result is expected to be an interface of the script). @@ -58,14 +74,17 @@ namespace LuaUtil void dropScriptCache() { mCompiledScripts.clear(); } + const ScriptsConfiguration& getConfiguration() const { return *mConf; } + private: static sol::protected_function_result throwIfError(sol::protected_function_result&&); template - friend sol::protected_function_result call(sol::protected_function fn, Args&&... args); + friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args); - sol::protected_function loadScript(const std::string& path); + sol::function loadScript(const std::string& path); sol::state mLua; + const ScriptsConfiguration* mConf; sol::table mSandboxEnv; std::map mCompiledScripts; std::map mCommonPackages; @@ -75,7 +94,7 @@ namespace LuaUtil // Should be used for every call of every Lua function. // It is a workaround for a bug in `sol`. See https://github.com/ThePhD/sol2/issues/1078 template - sol::protected_function_result call(sol::protected_function fn, Args&&... args) + sol::protected_function_result call(const sol::protected_function& fn, Args&&... args) { try { @@ -101,7 +120,7 @@ namespace LuaUtil std::string toString(const sol::object&); // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata. - // Needed to forbid any changes in common resources that can accessed from different sandboxes. + // Needed to forbid any changes in common resources that can be accessed from different sandboxes. sol::table makeReadOnly(sol::table); sol::table getMutableFromReadOnly(const sol::userdata&); diff --git a/components/lua/omwscriptsparser.cpp b/components/lua/omwscriptsparser.cpp deleted file mode 100644 index bc73e013db..0000000000 --- a/components/lua/omwscriptsparser.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "omwscriptsparser.hpp" - -#include - -#include - -std::vector LuaUtil::parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists) -{ - auto endsWith = [](std::string_view s, std::string_view suffix) - { - return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); - }; - std::vector res; - for (const std::string& scriptListFile : scriptLists) - { - if (!endsWith(scriptListFile, ".omwscripts")) - { - Log(Debug::Error) << "Script list should have suffix '.omwscripts', got: '" << scriptListFile << "'"; - continue; - } - std::string content(std::istreambuf_iterator(*vfs->get(scriptListFile)), {}); - std::string_view view(content); - while (!view.empty()) - { - size_t pos = 0; - while (pos < view.size() && view[pos] != '\n') - pos++; - std::string_view line = view.substr(0, pos); - view = view.substr(std::min(pos + 1, view.size())); - if (!line.empty() && line.back() == '\r') - line = line.substr(0, pos - 1); - // Lines starting with '#' are comments. - // TODO: Maybe make the parser more robust. It is a bit inconsistent that 'path/#to/file.lua' - // is a valid path, but '#path/to/file.lua' is considered as a comment and ignored. - if (line.empty() || line[0] == '#') - continue; - if (endsWith(line, ".lua")) - res.push_back(std::string(line)); - else - Log(Debug::Error) << "Lua script should have suffix '.lua', got: '" << line.substr(0, 300) << "'"; - } - } - return res; -} diff --git a/components/lua/omwscriptsparser.hpp b/components/lua/omwscriptsparser.hpp deleted file mode 100644 index 1da9f123b2..0000000000 --- a/components/lua/omwscriptsparser.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef COMPONENTS_LUA_OMWSCRIPTSPARSER_H -#define COMPONENTS_LUA_OMWSCRIPTSPARSER_H - -#include - -namespace LuaUtil -{ - - // Parses list of `*.omwscripts` files. - std::vector parseOMWScriptsFiles(const VFS::Manager* vfs, const std::vector& scriptLists); - -} - -#endif // COMPONENTS_LUA_OMWSCRIPTSPARSER_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 703381a453..eb9da7a60b 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -10,162 +10,249 @@ namespace LuaUtil static constexpr std::string_view INTERFACE_NAME = "interfaceName"; static constexpr std::string_view INTERFACE = "interface"; + static constexpr std::string_view HANDLER_INIT = "onInit"; static constexpr std::string_view HANDLER_SAVE = "onSave"; static constexpr std::string_view HANDLER_LOAD = "onLoad"; + static constexpr std::string_view HANDLER_INTERFACE_OVERRIDE = "onInterfaceOverride"; - static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; - static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode) + : mNamePrefix(namePrefix), mLua(*lua), mAutoStartMode(autoStartMode) + { + registerEngineHandlers({&mUpdateHandlers}); + mPublicInterfaces = sol::table(lua->sol(), sol::create); + addPackage("openmw.interfaces", mPublicInterfaces); + } - std::string ScriptsContainer::ScriptId::toString() const + void ScriptsContainer::printError(int scriptId, std::string_view msg, const std::exception& e) { - std::string res = mContainer->mNamePrefix; - res.push_back('['); - res.append(mPath); - res.push_back(']'); - return res; + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(scriptId) << "] " << msg << ": " << e.what(); } - ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) + void ScriptsContainer::addPackage(std::string packageName, sol::object package) { - registerEngineHandlers({&mUpdateHandlers}); - mPublicInterfaces = sol::table(lua->sol(), sol::create); - addPackage("openmw.interfaces", mPublicInterfaces); + mAPI.emplace(std::move(packageName), makeReadOnly(std::move(package))); + } + + bool ScriptsContainer::addCustomScript(int scriptId) + { + assert(mLua.getConfiguration()[scriptId].mFlags & ESM::LuaScriptCfg::sCustom); + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + return ok; } - void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) + void ScriptsContainer::addAutoStartedScripts() { - API[packageName] = makeReadOnly(std::move(package)); + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + { + std::optional onInit, onLoad; + bool ok = addScript(scriptId, onInit, onLoad); + if (ok && onInit) + callOnInit(scriptId, *onInit); + } } - bool ScriptsContainer::addNewScript(const std::string& path) + bool ScriptsContainer::addScript(int scriptId, std::optional& onInit, std::optional& onLoad) { - if (mScripts.count(path) != 0) + assert(scriptId >= 0 && scriptId < static_cast(mLua.getConfiguration().size())); + if (mScripts.count(scriptId) != 0) return false; // already present + const std::string& path = scriptPath(scriptId); + std::string debugName = mNamePrefix; + debugName.push_back('['); + debugName.append(path); + debugName.push_back(']'); + + Script& script = mScripts[scriptId]; + script.mHiddenData = mLua.newTable(); + script.mHiddenData[sScriptIdKey] = ScriptId{this, scriptId}; + script.mHiddenData[sScriptDebugNameKey] = debugName; + script.mPath = path; + try { - sol::table hiddenData(mLua.sol(), sol::create); - hiddenData[ScriptId::KEY] = ScriptId{this, path}; - hiddenData[REGISTERED_TIMER_CALLBACKS] = mLua.newTable(); - hiddenData[TEMPORARY_TIMER_CALLBACKS] = mLua.newTable(); - mScripts[path].mHiddenData = hiddenData; - sol::object script = mLua.runInNewSandbox(path, mNamePrefix, API, hiddenData); - std::string interfaceName = ""; - sol::object publicInterface = sol::nil; - if (script != sol::nil) + sol::object scriptOutput = mLua.runInNewSandbox(path, mNamePrefix, mAPI, script.mHiddenData); + if (scriptOutput == sol::nil) + return true; + sol::object engineHandlers = sol::nil, eventHandlers = sol::nil; + for (const auto& [key, value] : sol::table(scriptOutput)) { - for (auto& [key, value] : sol::table(script)) + std::string_view sectionName = key.as(); + if (sectionName == ENGINE_HANDLERS) + engineHandlers = value; + else if (sectionName == EVENT_HANDLERS) + eventHandlers = value; + else if (sectionName == INTERFACE_NAME) + script.mInterfaceName = value.as(); + else if (sectionName == INTERFACE) + script.mInterface = value.as(); + else + Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << debugName; + } + if (engineHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(engineHandlers)) { - std::string_view sectionName = key.as(); - if (sectionName == ENGINE_HANDLERS) - parseEngineHandlers(value, path); - else if (sectionName == EVENT_HANDLERS) - parseEventHandlers(value, path); - else if (sectionName == INTERFACE_NAME) - interfaceName = value.as(); - else if (sectionName == INTERFACE) - publicInterface = value.as(); + std::string_view handlerName = key.as(); + if (handlerName == HANDLER_INIT) + onInit = sol::function(fn); + else if (handlerName == HANDLER_LOAD) + onLoad = sol::function(fn); + else if (handlerName == HANDLER_SAVE) + script.mOnSave = sol::function(fn); + else if (handlerName == HANDLER_INTERFACE_OVERRIDE) + script.mOnOverride = sol::function(fn); else - Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << mNamePrefix << "[" << path << "]"; + { + auto it = mEngineHandlers.find(handlerName); + if (it == mEngineHandlers.end()) + Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << debugName; + else + insertHandler(it->second->mList, scriptId, fn); + } + } + } + if (eventHandlers != sol::nil) + { + for (const auto& [key, fn] : sol::table(eventHandlers)) + { + std::string_view eventName = key.as(); + auto it = mEventHandlers.find(eventName); + if (it == mEventHandlers.end()) + it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first; + insertHandler(it->second, scriptId, fn); } } - if (interfaceName.empty() != (publicInterface == sol::nil)) - Log(Debug::Error) << mNamePrefix << "[" << path << "]: 'interfaceName' should always be used together with 'interface'"; - else if (!interfaceName.empty()) - script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); - mScriptOrder.push_back(path); - mScripts[path].mInterface = std::move(script); + + if (script.mInterfaceName.empty() == script.mInterface.has_value()) + { + Log(Debug::Error) << debugName << ": 'interfaceName' should always be used together with 'interface'"; + script.mInterfaceName.clear(); + script.mInterface = sol::nil; + } + else if (script.mInterface) + { + script.mInterface = makeReadOnly(*script.mInterface); + insertInterface(scriptId, script); + } + return true; } catch (std::exception& e) { - mScripts.erase(path); - Log(Debug::Error) << "Can't start " << mNamePrefix << "[" << path << "]; " << e.what(); + mScripts[scriptId].mHiddenData[sScriptIdKey] = sol::nil; + mScripts.erase(scriptId); + Log(Debug::Error) << "Can't start " << debugName << "; " << e.what(); return false; } } - bool ScriptsContainer::removeScript(const std::string& path) + void ScriptsContainer::removeScript(int scriptId) { - auto scriptIter = mScripts.find(path); + auto scriptIter = mScripts.find(scriptId); if (scriptIter == mScripts.end()) - return false; // no such script - scriptIter->second.mHiddenData[ScriptId::KEY] = sol::nil; - sol::object& script = scriptIter->second.mInterface; - if (getFieldOrNil(script, INTERFACE_NAME) != sol::nil) + return; // no such script + Script& script = scriptIter->second; + if (script.mInterface) + removeInterface(scriptId, script); + script.mHiddenData[sScriptIdKey] = sol::nil; + mScripts.erase(scriptIter); + for (auto& [_, handlers] : mEngineHandlers) + removeHandler(handlers->mList, scriptId); + for (auto& [_, handlers] : mEventHandlers) + removeHandler(handlers, scriptId); + } + + void ScriptsContainer::insertInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) { - std::string_view interfaceName = getFieldOrNil(script, INTERFACE_NAME).as(); - if (mPublicInterfaces[interfaceName] == getFieldOrNil(script, INTERFACE)) + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else { - mPublicInterfaces[interfaceName] = sol::nil; - auto prevIt = mScriptOrder.rbegin(); - while (*prevIt != path) - prevIt++; - prevIt++; - while (prevIt != mScriptOrder.rend()) - { - sol::object& prevScript = mScripts[*(prevIt++)].mInterface; - sol::object prevInterfaceName = getFieldOrNil(prevScript, INTERFACE_NAME); - if (prevInterfaceName != sol::nil && prevInterfaceName.as() == interfaceName) - { - mPublicInterfaces[interfaceName] = getFieldOrNil(prevScript, INTERFACE); - break; - } - } + next = &otherScript; + nextId = otherId; + break; } } - sol::object engineHandlers = getFieldOrNil(script, ENGINE_HANDLERS); - if (engineHandlers != sol::nil) + if (prev && script.mOnOverride) + { + try { LuaUtil::call(*script.mOnOverride, *prev->mInterface); } + catch (std::exception& e) { printError(scriptId, "onInterfaceOverride failed", e); } + } + if (next && next->mOnOverride) + { + try { LuaUtil::call(*next->mOnOverride, *script.mInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } + } + if (next == nullptr) + mPublicInterfaces[script.mInterfaceName] = *script.mInterface; + } + + void ScriptsContainer::removeInterface(int scriptId, const Script& script) + { + assert(script.mInterface); + const Script* prev = nullptr; + const Script* next = nullptr; + int nextId = 0; + for (const auto& [otherId, otherScript] : mScripts) { - for (auto& [key, value] : sol::table(engineHandlers)) + if (scriptId == otherId || script.mInterfaceName != otherScript.mInterfaceName) + continue; + if (otherId < scriptId) + prev = &otherScript; + else { - std::string_view handlerName = key.as(); - auto handlerIter = mEngineHandlers.find(handlerName); - if (handlerIter == mEngineHandlers.end()) - continue; - std::vector& list = handlerIter->second->mList; - list.erase(std::find(list.begin(), list.end(), value.as())); + next = &otherScript; + nextId = otherId; + break; } } - sol::object eventHandlers = getFieldOrNil(script, EVENT_HANDLERS); - if (eventHandlers != sol::nil) + if (next) { - for (auto& [key, value] : sol::table(eventHandlers)) + if (next->mOnOverride) { - EventHandlerList& list = mEventHandlers.find(key.as())->second; - list.erase(std::find(list.begin(), list.end(), value.as())); + sol::object prevInterface = sol::nil; + if (prev) + prevInterface = *prev->mInterface; + try { LuaUtil::call(*next->mOnOverride, prevInterface); } + catch (std::exception& e) { printError(nextId, "onInterfaceOverride failed", e); } } } - mScripts.erase(scriptIter); - mScriptOrder.erase(std::find(mScriptOrder.begin(), mScriptOrder.end(), path)); - return true; + else if (prev) + mPublicInterfaces[script.mInterfaceName] = *prev->mInterface; + else + mPublicInterfaces[script.mInterfaceName] = sol::nil; } - void ScriptsContainer::parseEventHandlers(sol::table handlers, std::string_view scriptPath) + void ScriptsContainer::insertHandler(std::vector& list, int scriptId, sol::function fn) { - for (auto& [key, value] : handlers) + list.emplace_back(); + int pos = list.size() - 1; + while (pos > 0 && list[pos - 1].mScriptId > scriptId) { - std::string_view eventName = key.as(); - auto it = mEventHandlers.find(eventName); - if (it == mEventHandlers.end()) - it = mEventHandlers.insert({std::string(eventName), EventHandlerList()}).first; - it->second.push_back(value); + list[pos] = std::move(list[pos - 1]); + pos--; } + list[pos].mScriptId = scriptId; + list[pos].mFn = std::move(fn); } - void ScriptsContainer::parseEngineHandlers(sol::table handlers, std::string_view scriptPath) + void ScriptsContainer::removeHandler(std::vector& list, int scriptId) { - for (auto& [key, value] : handlers) - { - std::string_view handlerName = key.as(); - if (handlerName == HANDLER_LOAD || handlerName == HANDLER_SAVE) - continue; // save and load are handled separately - auto it = mEngineHandlers.find(handlerName); - if (it == mEngineHandlers.end()) - Log(Debug::Error) << "Not supported handler '" << handlerName << "' in " << mNamePrefix << "[" << scriptPath << "]"; - else - it->second->mList.push_back(value); - } + list.erase(std::remove_if(list.begin(), list.end(), + [scriptId](const Handler& h){ return h.mScriptId == scriptId; }), + list.end()); } void ScriptsContainer::receiveEvent(std::string_view eventName, std::string_view eventData) @@ -191,13 +278,14 @@ namespace LuaUtil { try { - sol::object res = LuaUtil::call(list[i], data); + sol::object res = LuaUtil::call(list[i].mFn, data); if (res != sol::nil && !res.as()) break; // Skip other handlers if 'false' was returned. } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " eventHandler[" << eventName << "] failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(list[i].mScriptId) + << "] eventHandler[" << eventName << "] failed. " << e.what(); } } } @@ -208,9 +296,19 @@ namespace LuaUtil mEngineHandlers[h->mName] = h; } + void ScriptsContainer::callOnInit(int scriptId, const sol::function& onInit) + { + try + { + const std::string& data = mLua.getConfiguration()[scriptId].mInitializationData; + LuaUtil::call(onInit, deserialize(mLua.sol(), data, mSerializer)); + } + catch (std::exception& e) { printError(scriptId, "onInit failed", e); } + } + void ScriptsContainer::save(ESM::LuaScripts& data) { - std::map> timers; + std::map> timers; auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) { if (!timer.mSerializable) @@ -220,78 +318,87 @@ namespace LuaUtil savedTimer.mUnit = timeUnit; savedTimer.mCallbackName = std::get(timer.mCallback); savedTimer.mCallbackArgument = timer.mSerializedArg; - if (timers.count(timer.mScript) == 0) - timers[timer.mScript] = {}; - timers[timer.mScript].push_back(std::move(savedTimer)); + timers[timer.mScriptId].push_back(std::move(savedTimer)); }; for (const Timer& timer : mSecondsTimersQueue) saveTimerFn(timer, TimeUnit::SECONDS); for (const Timer& timer : mHoursTimersQueue) saveTimerFn(timer, TimeUnit::HOURS); data.mScripts.clear(); - for (const std::string& path : mScriptOrder) + for (auto& [scriptId, script] : mScripts) { ESM::LuaScript savedScript; - savedScript.mScriptPath = path; - sol::object handler = getFieldOrNil(mScripts[path].mInterface, ENGINE_HANDLERS, HANDLER_SAVE); - if (handler != sol::nil) + // Note: We can not use `scriptPath(scriptId)` here because `save` can be called during + // evaluating "reloadlua" command when ScriptsConfiguration is already changed. + savedScript.mScriptPath = script.mPath; + if (script.mOnSave) { try { - sol::object state = LuaUtil::call(handler); + sol::object state = LuaUtil::call(*script.mOnSave); savedScript.mData = serialize(state, mSerializer); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onSave failed: " << e.what(); - } + catch (std::exception& e) { printError(scriptId, "onSave failed", e); } } - auto timersIt = timers.find(path); + auto timersIt = timers.find(scriptId); if (timersIt != timers.end()) savedScript.mTimers = std::move(timersIt->second); data.mScripts.push_back(std::move(savedScript)); } } - void ScriptsContainer::load(const ESM::LuaScripts& data, bool resetScriptList) + void ScriptsContainer::load(const ESM::LuaScripts& data) { - std::map scriptsWithoutSavedData; - if (resetScriptList) + removeAllScripts(); + const ScriptsConfiguration& cfg = mLua.getConfiguration(); + + std::map scripts; + for (int scriptId : mLua.getConfiguration().getListByFlag(mAutoStartMode)) + scripts[scriptId] = nullptr; + for (const ESM::LuaScript& s : data.mScripts) { - removeAllScripts(); - for (const ESM::LuaScript& script : data.mScripts) - addNewScript(script.mScriptPath); + std::optional scriptId = cfg.findId(s.mScriptPath); + if (!scriptId) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; script not registered"; + continue; + } + if (!(cfg[*scriptId].mFlags & (ESM::LuaScriptCfg::sCustom | mAutoStartMode))) + { + Log(Debug::Verbose) << "Ignoring " << mNamePrefix << "[" << s.mScriptPath << "]; this script is not allowed here"; + continue; + } + scripts[*scriptId] = &s; } - else - scriptsWithoutSavedData = mScripts; - mSecondsTimersQueue.clear(); - mHoursTimersQueue.clear(); - for (const ESM::LuaScript& script : data.mScripts) + + for (const auto& [scriptId, savedScript] : scripts) { - auto iter = mScripts.find(script.mScriptPath); - if (iter == mScripts.end()) + std::optional onInit, onLoad; + if (!addScript(scriptId, onInit, onLoad)) continue; - scriptsWithoutSavedData.erase(iter->first); - iter->second.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - try + if (savedScript == nullptr) { - sol::object handler = getFieldOrNil(iter->second.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler != sol::nil) - { - sol::object state = deserialize(mLua.sol(), script.mData, mSerializer); - LuaUtil::call(handler, state); - } + if (onInit) + callOnInit(scriptId, *onInit); + continue; } - catch (std::exception& e) + if (onLoad) { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] onLoad failed: " << e.what(); + try + { + sol::object state = deserialize(mLua.sol(), savedScript->mData, mSerializer); + sol::object initializationData = + deserialize(mLua.sol(), mLua.getConfiguration()[scriptId].mInitializationData, mSerializer); + LuaUtil::call(*onLoad, state, initializationData); + } + catch (std::exception& e) { printError(scriptId, "onLoad failed", e); } } - for (const ESM::LuaTimer& savedTimer : script.mTimers) + for (const ESM::LuaTimer& savedTimer : savedScript->mTimers) { Timer timer; timer.mCallback = savedTimer.mCallbackName; timer.mSerializable = true; - timer.mScript = script.mScriptPath; + timer.mScriptId = scriptId; timer.mTime = savedTimer.mTime; try @@ -306,24 +413,10 @@ namespace LuaUtil else mSecondsTimersQueue.push_back(std::move(timer)); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << script.mScriptPath << "] can not load timer: " << e.what(); - } - } - } - for (auto& [path, script] : scriptsWithoutSavedData) - { - script.mHiddenData.get(TEMPORARY_TIMER_CALLBACKS).clear(); - sol::object handler = getFieldOrNil(script.mInterface, ENGINE_HANDLERS, HANDLER_LOAD); - if (handler == sol::nil) - continue; - try { LuaUtil::call(handler); } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << path << "] onLoad failed: " << e.what(); + catch (std::exception& e) { printError(scriptId, "can not load timer", e); } } } + std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } @@ -331,15 +424,16 @@ namespace LuaUtil ScriptsContainer::~ScriptsContainer() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; } + // Note: shouldn't be called from destructor because mEngineHandlers has pointers on + // external objects that are already removed during child class destruction. void ScriptsContainer::removeAllScripts() { for (auto& [_, script] : mScripts) - script.mHiddenData[ScriptId::KEY] = sol::nil; + script.mHiddenData[sScriptIdKey] = sol::nil; mScripts.clear(); - mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); mEventHandlers.clear(); @@ -351,17 +445,17 @@ namespace LuaUtil mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } - sol::table ScriptsContainer::getHiddenData(const std::string& scriptPath) + ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId) { - auto it = mScripts.find(scriptPath); + auto it = mScripts.find(scriptId); if (it == mScripts.end()) - throw std::logic_error("ScriptsContainer::getHiddenData: script doesn't exist"); - return it->second.mHiddenData; + throw std::logic_error("Script doesn't exist"); + return it->second; } - void ScriptsContainer::registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback) + void ScriptsContainer::registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback) { - getHiddenData(scriptPath)[REGISTERED_TIMER_CALLBACKS][callbackName] = std::move(callback); + getScript(scriptId).mRegisteredCallbacks.emplace(std::string(callbackName), std::move(callback)); } void ScriptsContainer::insertTimer(std::vector& timerQueue, Timer&& t) @@ -370,12 +464,12 @@ namespace LuaUtil std::push_heap(timerQueue.begin(), timerQueue.end()); } - void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg) { Timer t; t.mCallback = std::string(callbackName); - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = true; t.mTime = time; t.mArg = callbackArg; @@ -383,15 +477,15 @@ namespace LuaUtil insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); } - void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback) + void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback) { Timer t; - t.mScript = scriptPath; + t.mScriptId = scriptId; t.mSerializable = false; t.mTime = time; t.mCallback = mTemporaryCallbackCounter; - getHiddenData(scriptPath)[TEMPORARY_TIMER_CALLBACKS][mTemporaryCallbackCounter] = std::move(callback); + getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback)); mTemporaryCallbackCounter++; insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); @@ -401,30 +495,23 @@ namespace LuaUtil { try { - sol::table data = getHiddenData(t.mScript); + Script& script = getScript(t.mScriptId); if (t.mSerializable) { const std::string& callbackName = std::get(t.mCallback); - sol::object callback = data[REGISTERED_TIMER_CALLBACKS][callbackName]; - if (!callback.is()) + auto it = script.mRegisteredCallbacks.find(callbackName); + if (it == script.mRegisteredCallbacks.end()) throw std::logic_error("Callback '" + callbackName + "' doesn't exist"); - LuaUtil::call(callback, t.mArg); + LuaUtil::call(it->second, t.mArg); } else { int64_t id = std::get(t.mCallback); - sol::table callbacks = data[TEMPORARY_TIMER_CALLBACKS]; - sol::object callback = callbacks[id]; - if (!callback.is()) - throw std::logic_error("Temporary timer callback doesn't exist"); - LuaUtil::call(callback); - callbacks[id] = sol::nil; + LuaUtil::call(script.mTemporaryCallbacks.at(id)); + script.mTemporaryCallbacks.erase(id); } } - catch (std::exception& e) - { - Log(Debug::Error) << mNamePrefix << "[" << t.mScript << "] callTimer failed: " << e.what(); - } + catch (std::exception& e) { printError(t.mScriptId, "callTimer failed", e); } } void ScriptsContainer::updateTimerQueue(std::vector& timerQueue, double time) diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 69aa18e940..1863d04669 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -17,7 +17,7 @@ namespace LuaUtil // ScriptsContainer is a base class for all scripts containers (LocalScripts, // GlobalScripts, PlayerScripts, etc). Each script runs in a separate sandbox. // Scripts from different containers can interact to each other only via events. -// Scripts within one container can interact via interfaces (not implemented yet). +// Scripts within one container can interact via interfaces. // All scripts from one container have the same set of API packages available. // // Each script should return a table in a specific format that describes its @@ -42,11 +42,12 @@ namespace LuaUtil // -- An error is printed if unknown handler is specified. // engineHandlers = { // onUpdate = update, +// onInit = function(initData) ... end, -- used when the script is just created (not loaded) // onSave = function() return ... end, -// onLoad = function(state) ... end, -- "state" is the data that was earlier returned by onSave +// onLoad = function(state, initData) ... end, -- "state" is the data that was earlier returned by onSave // -// -- Works only if ScriptsContainer::registerEngineHandler is overloaded in a child class -// -- and explicitly supports 'onSomethingElse' +// -- Works only if a child class has passed a EngineHandlerList +// -- for 'onSomethingElse' to ScriptsContainer::registerEngineHandlers. // onSomethingElse = function() print("something else") end // }, // @@ -59,36 +60,44 @@ namespace LuaUtil class ScriptsContainer { public: + // ScriptId of each script is stored with this key in Script::mHiddenData. + // Removed from mHiddenData when the script if removed. + constexpr static std::string_view sScriptIdKey = "_id"; + + // Debug identifier of each script is stored with this key in Script::mHiddenData. + // Present in mHiddenData even after removal of the script from ScriptsContainer. + constexpr static std::string_view sScriptDebugNameKey = "_name"; + struct ScriptId { - // ScriptId is stored in hidden data (see getHiddenData) with this key. - constexpr static std::string_view KEY = "_id"; - ScriptsContainer* mContainer; - std::string mPath; - - std::string toString() const; + int mIndex; // index in LuaUtil::ScriptsConfiguration }; using TimeUnit = ESM::LuaTimer::TimeUnit; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. - ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); + // `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is + // stored in ScriptsConfiguration: lua->getConfiguration().getListByFlag(autoStartMode). + ScriptsContainer(LuaState* lua, std::string_view namePrefix, ESM::LuaScriptCfg::Flags autoStartMode = 0); + ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; virtual ~ScriptsContainer(); + ESM::LuaScriptCfg::Flags getAutoStartMode() const { return mAutoStartMode; } + // Adds package that will be available (via `require`) for all scripts in the container. // Automatically applies LuaUtil::makeReadOnly to the package. - void addPackage(const std::string& packageName, sol::object package); + void addPackage(std::string packageName, sol::object package); - // Finds a file with given path in the virtual file system, starts as a new script, and adds it to the container. - // Returns `true` if the script was successfully added. Otherwise prints an error message and returns `false`. - // `false` can be returned if either file not found or has syntax errors or such script already exists in the container. - bool addNewScript(const std::string& path); + // Gets script with given id from ScriptsConfiguration, finds the source in the virtual file system, starts as a new script, + // adds it to the container, and calls onInit for this script. Returns `true` if the script was successfully added. + // The script should have CUSTOM flag. If the flag is not set, or file not found, or has syntax errors, returns false. + // If such script already exists in the container, then also returns false. + bool addCustomScript(int scriptId); - // Removes script. Returns `true` if it was successfully removed. - bool removeScript(const std::string& path); - void removeAllScripts(); + bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } + void removeScript(int scriptId); // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. void processTimers(double gameSeconds, double gameHours); @@ -107,22 +116,22 @@ namespace LuaUtil // only built-in types and types from util package can be serialized. void setSerializer(const UserdataSerializer* serializer) { mSerializer = serializer; } + // Starts scripts according to `autoStartMode` and calls `onInit` for them. Not needed if `load` is used. + void addAutoStartedScripts(); + + // Removes all scripts including the auto started. + void removeAllScripts(); + // Calls engineHandler "onSave" for every script and saves the list of the scripts with serialized data to ESM::LuaScripts. void save(ESM::LuaScripts&); - // Calls engineHandler "onLoad" for every script with given data. - // If resetScriptList=true, then removes all currently active scripts and runs the scripts that were saved in ESM::LuaScripts. - // If resetScriptList=false, then list of running scripts is not changed, only engineHandlers "onLoad" are called. - void load(const ESM::LuaScripts&, bool resetScriptList); - - // Returns the hidden data of a script. - // Each script has a corresponding "hidden data" - a lua table that is not accessible from the script itself, - // but can be used by built-in packages. It contains ScriptId and can contain any arbitrary data. - sol::table getHiddenData(const std::string& scriptPath); + // Removes all scripts; starts scripts according to `autoStartMode` and + // loads the savedScripts. Runs "onLoad" for each script. + void load(const ESM::LuaScripts& savedScripts); // Callbacks for serializable timers should be registered in advance. // The script with the given path should already present in the container. - void registerTimerCallback(const std::string& scriptPath, std::string_view callbackName, sol::function callback); + void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); // Sets up a timer, that can be automatically saved and loaded. // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). @@ -130,18 +139,24 @@ namespace LuaUtil // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. // callbackName - callback (should be registered in advance) for this timer. // callbackArg - parameter for the callback (should be serializable). - void setupSerializableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, + void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, std::string_view callbackName, sol::object callbackArg); // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. - void setupUnsavableTimer(TimeUnit timeUnit, double time, const std::string& scriptPath, sol::function callback); + void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback); protected: + struct Handler + { + int mScriptId; + sol::function mFn; + }; + struct EngineHandlerList { std::string_view mName; - std::vector mList; + std::vector mList; // "name" must be string literal explicit EngineHandlerList(std::string_view name) : mName(name) {} @@ -151,12 +166,13 @@ namespace LuaUtil template void callEngineHandlers(EngineHandlerList& handlers, const Args&... args) { - for (sol::protected_function& handler : handlers.mList) + for (Handler& handler : handlers.mList) { - try { LuaUtil::call(handler, args...); } + try { LuaUtil::call(handler.mFn, args...); } catch (std::exception& e) { - Log(Debug::Error) << mNamePrefix << " " << handlers.mName << " failed. " << e.what(); + Log(Debug::Error) << mNamePrefix << "[" << scriptPath(handler.mScriptId) << "] " + << handlers.mName << " failed. " << e.what(); } } } @@ -171,34 +187,50 @@ namespace LuaUtil private: struct Script { - sol::object mInterface; // returned value of the script (sol::table or nil) + std::optional mOnSave; + std::optional mOnOverride; + std::optional mInterface; + std::string mInterfaceName; sol::table mHiddenData; + std::map mRegisteredCallbacks; + std::map mTemporaryCallbacks; + std::string mPath; }; struct Timer { double mTime; bool mSerializable; - std::string mScript; + int mScriptId; std::variant mCallback; // string if serializable, integer otherwise sol::object mArg; std::string mSerializedArg; bool operator<(const Timer& t) const { return mTime > t.mTime; } }; - using EventHandlerList = std::vector; + using EventHandlerList = std::vector; + + // Add to container without calling onInit/onLoad. + bool addScript(int scriptId, std::optional& onInit, std::optional& onLoad); - void parseEngineHandlers(sol::table handlers, std::string_view scriptPath); - void parseEventHandlers(sol::table handlers, std::string_view scriptPath); + // Returns script by id (throws an exception if doesn't exist) + Script& getScript(int scriptId); + void printError(int scriptId, std::string_view msg, const std::exception& e); + const std::string& scriptPath(int scriptId) const { return mLua.getConfiguration()[scriptId].mScriptPath; } + void callOnInit(int scriptId, const sol::function& onInit); void callTimer(const Timer& t); void updateTimerQueue(std::vector& timerQueue, double time); static void insertTimer(std::vector& timerQueue, Timer&& t); + static void insertHandler(std::vector& list, int scriptId, sol::function fn); + static void removeHandler(std::vector& list, int scriptId); + void insertInterface(int scriptId, const Script& script); + void removeInterface(int scriptId, const Script& script); + ESM::LuaScriptCfg::Flags mAutoStartMode; const UserdataSerializer* mSerializer = nullptr; - std::map API; + std::map mAPI; - std::vector mScriptOrder; - std::map mScripts; + std::map mScripts; sol::table mPublicInterfaces; EngineHandlerList mUpdateHandlers{"onUpdate"}; @@ -210,6 +242,25 @@ namespace LuaUtil int64_t mTemporaryCallbackCounter = 0; }; + // Wrapper for a Lua function. + // Holds information about the script the function belongs to. + // Needed to prevent callback calls if the script was removed. + struct Callback + { + sol::function mFunc; + sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer + + template + void operator()(Args&&... args) const + { + if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil) + LuaUtil::call(mFunc, std::forward(args)...); + else + Log(Debug::Debug) << "Ignored callback to the removed script " + << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); + } + }; + } #endif // COMPONENTS_LUA_SCRIPTSCONTAINER_H diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 17cb64461a..b68fc7afa4 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -105,9 +105,9 @@ namespace LuaUtil [](const Vec3& v) { return TransformM{osg::Matrixf::scale(v)}; }, [](float x, float y, float z) { return TransformM{osg::Matrixf::scale(x, y, z)}; }); transforms["rotate"] = [](float angle, const Vec3& axis) { return TransformQ{osg::Quat(angle, axis)}; }; - transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(1, 0, 0))}; }; - transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 1, 0))}; }; - transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, 1))}; }; + transforms["rotateX"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(-1, 0, 0))}; }; + transforms["rotateY"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, -1, 0))}; }; + transforms["rotateZ"] = [](float angle) { return TransformQ{osg::Quat(angle, Vec3(0, 0, -1))}; }; transMType[sol::meta_function::multiplication] = sol::overload( [](const TransformM& a, const Vec3& b) { return a.mM.preMult(b); }, diff --git a/components/lua_ui/content.cpp b/components/lua_ui/content.cpp new file mode 100644 index 0000000000..6f9cf61f2f --- /dev/null +++ b/components/lua_ui/content.cpp @@ -0,0 +1,106 @@ +#include "content.hpp" + +namespace LuaUi +{ + Content::Content(const sol::table& table) + { + size_t size = table.size(); + for (size_t index = 0; index < size; ++index) + { + sol::object value = table.get(index + 1); + if (value.is()) + assign(index, value.as()); + else + throw std::logic_error("UI Content children must all be tables."); + } + } + + void Content::assign(size_t index, const sol::table& table) + { + if (mOrdered.size() < index) + throw std::logic_error("Can't have gaps in UI Content."); + if (index == mOrdered.size()) + mOrdered.push_back(table); + else + { + sol::optional oldName = mOrdered[index]["name"]; + if (oldName.has_value()) + mNamed.erase(oldName.value()); + mOrdered[index] = table; + } + sol::optional name = table["name"]; + if (name.has_value()) + mNamed[name.value()] = index; + } + + void Content::assign(std::string_view name, const sol::table& table) + { + auto it = mNamed.find(name); + if (it != mNamed.end()) + assign(it->second, table); + else + throw std::logic_error(std::string("Can't find a UI Content child with name ") += name); + } + + void Content::insert(size_t index, const sol::table& table) + { + size_t size = mOrdered.size(); + if (size < index) + throw std::logic_error("Can't have gaps in UI Content."); + mOrdered.insert(mOrdered.begin() + index, table); + for (size_t i = index; i < size; ++i) + { + sol::optional name = mOrdered[i]["name"]; + if (name.has_value()) + mNamed[name.value()] = index; + } + } + + sol::table Content::at(size_t index) const + { + if (index > size()) + throw std::logic_error("Invalid UI Content index."); + return mOrdered.at(index); + } + + sol::table Content::at(std::string_view name) const + { + auto it = mNamed.find(name); + if (it == mNamed.end()) + throw std::logic_error("Invalid UI Content name."); + return mOrdered.at(it->second); + } + + size_t Content::remove(size_t index) + { + sol::table table = at(index); + sol::optional name = table["name"]; + if (name.has_value()) + { + auto it = mNamed.find(name.value()); + if (it != mNamed.end()) + mNamed.erase(it); + } + mOrdered.erase(mOrdered.begin() + index); + return index; + } + + size_t Content::remove(std::string_view name) + { + auto it = mNamed.find(name); + if (it == mNamed.end()) + throw std::logic_error("Invalid UI Content name."); + size_t index = it->second; + remove(index); + return index; + } + + size_t Content::indexOf(const sol::table& table) + { + auto it = std::find(mOrdered.begin(), mOrdered.end(), table); + if (it == mOrdered.end()) + return size(); + else + return it - mOrdered.begin(); + } +} diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp new file mode 100644 index 0000000000..e970744b3d --- /dev/null +++ b/components/lua_ui/content.hpp @@ -0,0 +1,41 @@ +#ifndef COMPONENTS_LUAUI_CONTENT +#define COMPONENTS_LUAUI_CONTENT + +#include +#include + +#include + +namespace LuaUi +{ + class Content + { + public: + using iterator = std::vector::iterator; + + Content() {} + + // expects a Lua array - a table with keys from 1 to n without any nil values in between + // any other keys are ignored + explicit Content(const sol::table&); + + size_t size() const { return mOrdered.size(); } + + void assign(std::string_view name, const sol::table& table); + void assign(size_t index, const sol::table& table); + void insert(size_t index, const sol::table& table); + + sol::table at(size_t index) const; + sol::table at(std::string_view name) const; + size_t remove(size_t index); + size_t remove(std::string_view name); + size_t indexOf(const sol::table& table); + + private: + std::map> mNamed; + std::vector mOrdered; + }; + +} + +#endif // COMPONENTS_LUAUI_CONTENT diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp new file mode 100644 index 0000000000..63ae0e7d5e --- /dev/null +++ b/components/lua_ui/element.cpp @@ -0,0 +1,147 @@ +#include "element.hpp" + +#include + +#include "content.hpp" +#include "widgetlist.hpp" + +namespace LuaUi +{ + + std::string widgetType(const sol::table& layout) + { + return layout.get_or("type", std::string("LuaWidget")); + } + + Content content(const sol::table& layout) + { + auto optional = layout.get>("content"); + if (optional.has_value()) + return optional.value(); + else + return Content(); + } + + void setProperties(LuaUi::WidgetExtension* ext, const sol::table& layout) + { + ext->setProperties(layout.get("props")); + } + + void setEventCallbacks(LuaUi::WidgetExtension* ext, const sol::table& layout) + { + ext->clearCallbacks(); + auto events = layout.get>("events"); + if (events.has_value()) + { + events.value().for_each([ext](const sol::object& name, const sol::object& callback) + { + if (name.is() && callback.is()) + ext->setCallback(name.as(), callback.as()); + else if (!name.is()) + Log(Debug::Warning) << "UI event key must be a string"; + else if (!callback.is()) + Log(Debug::Warning) << "UI event handler for key \"" << name.as() + << "\" must be an openmw.async.callback"; + }); + } + } + + void setLayout(LuaUi::WidgetExtension* ext, const sol::table& layout) + { + ext->setLayout(layout); + } + + LuaUi::WidgetExtension* createWidget(const sol::table& layout, LuaUi::WidgetExtension* parent) + { + std::string type = widgetType(layout); + std::string skin = layout.get_or("skin", std::string()); + std::string layer = layout.get_or("layer", std::string("Windows")); + std::string name = layout.get_or("name", std::string()); + + static auto widgetTypeMap = widgetTypeToName(); + if (widgetTypeMap.find(type) == widgetTypeMap.end()) + throw std::logic_error(std::string("Invalid widget type ") += type); + + MyGUI::Widget* widget = MyGUI::Gui::getInstancePtr()->createWidgetT( + type, skin, + MyGUI::IntCoord(), MyGUI::Align::Default, + layer, name); + + LuaUi::WidgetExtension* ext = dynamic_cast(widget); + if (!ext) + throw std::runtime_error("Invalid widget!"); + + ext->create(layout.lua_state(), widget); + if (parent != nullptr) + widget->attachToWidget(parent->widget()); + + setEventCallbacks(ext, layout); + setProperties(ext, layout); + setLayout(ext, layout); + + Content cont = content(layout); + for (size_t i = 0; i < cont.size(); i++) + ext->addChild(createWidget(cont.at(i), ext)); + + return ext; + } + + void destroyWidget(LuaUi::WidgetExtension* ext) + { + ext->destroy(); + MyGUI::Gui::getInstancePtr()->destroyWidget(ext->widget()); + } + + void updateWidget(const sol::table& layout, LuaUi::WidgetExtension* ext) + { + setEventCallbacks(ext, layout); + setProperties(ext, layout); + setLayout(ext, layout); + + Content newContent = content(layout); + + size_t oldSize = ext->childCount(); + size_t newSize = newContent.size(); + size_t minSize = std::min(oldSize, newSize); + for (size_t i = 0; i < minSize; i++) + { + LuaUi::WidgetExtension* oldWidget = ext->childAt(i); + sol::table newChild = newContent.at(i); + + if (oldWidget->widget()->getTypeName() != widgetType(newChild)) + { + destroyWidget(oldWidget); + ext->assignChild(i, createWidget(newChild, ext)); + } + else + updateWidget(newChild, oldWidget); + } + + for (size_t i = minSize; i < oldSize; i++) + destroyWidget(ext->eraseChild(i)); + + for (size_t i = minSize; i < newSize; i++) + ext->addChild(createWidget(newContent.at(i), ext)); + } + + void Element::create() + { + assert(!mRoot); + if (!mRoot) + mRoot = createWidget(mLayout, nullptr); + } + + void Element::update() + { + if (mRoot && mUpdate) + updateWidget(mLayout, mRoot); + mUpdate = false; + } + + void Element::destroy() + { + if (mRoot) + destroyWidget(mRoot); + mRoot = nullptr; + } +} diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp new file mode 100644 index 0000000000..10e10d8960 --- /dev/null +++ b/components/lua_ui/element.hpp @@ -0,0 +1,31 @@ +#ifndef OPENMW_LUAUI_ELEMENT +#define OPENMW_LUAUI_ELEMENT + +#include "widget.hpp" + +namespace LuaUi +{ + struct Element + { + Element(sol::table layout) + : mRoot{ nullptr } + , mLayout{ layout } + , mUpdate{ false } + , mDestroy{ false } + { + } + + LuaUi::WidgetExtension* mRoot; + sol::table mLayout; + bool mUpdate; + bool mDestroy; + + void create(); + + void update(); + + void destroy(); + }; +} + +#endif // !OPENMW_LUAUI_ELEMENT diff --git a/components/lua_ui/properties.hpp b/components/lua_ui/properties.hpp new file mode 100644 index 0000000000..7f23a5ce6c --- /dev/null +++ b/components/lua_ui/properties.hpp @@ -0,0 +1,64 @@ +#ifndef OPENMW_LUAUI_PROPERTIES +#define OPENMW_LUAUI_PROPERTIES + +#include +#include +#include + +#include + +namespace LuaUi +{ + template + sol::optional getProperty(sol::object from, std::string_view field) { + sol::object value = LuaUtil::getFieldOrNil(from, field); + if (value == sol::nil) + return sol::nullopt; + if (value.is()) + return value.as(); + std::string error("Property \""); + error += field; + error += "\" has an invalid value \""; + error += LuaUtil::toString(value); + error += "\""; + throw std::logic_error(error); + } + + template + T parseProperty(sol::object from, std::string_view field, const T& defaultValue) + { + sol::optional opt = getProperty(from, field); + if (opt.has_value()) + return opt.value(); + else + return defaultValue; + } + + template + MyGUI::types::TPoint parseProperty( + sol::object from, + std::string_view field, + const MyGUI::types::TPoint& defaultValue) + { + auto v = getProperty(from, field); + if (v.has_value()) + return MyGUI::types::TPoint(v.value().x(), v.value().y()); + else + return defaultValue; + } + + template + MyGUI::types::TSize parseProperty( + sol::object from, + std::string_view field, + const MyGUI::types::TSize& defaultValue) + { + auto v = getProperty(from, field); + if (v.has_value()) + return MyGUI::types::TSize(v.value().x(), v.value().y()); + else + return defaultValue; + } +} + +#endif // !OPENMW_LUAUI_PROPERTIES diff --git a/components/lua_ui/text.cpp b/components/lua_ui/text.cpp new file mode 100644 index 0000000000..4ae9865ac3 --- /dev/null +++ b/components/lua_ui/text.cpp @@ -0,0 +1,29 @@ + +#include "text.hpp" + +namespace LuaUi +{ + LuaText::LuaText() + : mAutoSized(true) + {} + + void LuaText::initialize() + { + WidgetExtension::initialize(); + } + + void LuaText::setProperties(sol::object props) + { + setCaption(parseProperty(props, "caption", std::string())); + mAutoSized = parseProperty(props, "autoSize", true); + WidgetExtension::setProperties(props); + } + + MyGUI::IntSize LuaText::calculateSize() + { + if (mAutoSized) + return getTextSize(); + else + return WidgetExtension::calculateSize(); + } +} diff --git a/components/lua_ui/text.hpp b/components/lua_ui/text.hpp new file mode 100644 index 0000000000..d87a9001a2 --- /dev/null +++ b/components/lua_ui/text.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_LUAUI_TEXT +#define OPENMW_LUAUI_TEXT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaText : public MyGUI::TextBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaText) + + public: + LuaText(); + virtual void initialize() override; + virtual void setProperties(sol::object) override; + + private: + bool mAutoSized; + + protected: + virtual MyGUI::IntSize calculateSize() override; + }; +} + +#endif // OPENMW_LUAUI_TEXT diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp new file mode 100644 index 0000000000..9cdc716ce4 --- /dev/null +++ b/components/lua_ui/textedit.cpp @@ -0,0 +1,10 @@ +#include "textedit.hpp" + +namespace LuaUi +{ + void LuaTextEdit::setProperties(sol::object props) + { + setCaption(parseProperty(props, "caption", std::string())); + WidgetExtension::setProperties(props); + } +} diff --git a/components/lua_ui/textedit.hpp b/components/lua_ui/textedit.hpp new file mode 100644 index 0000000000..d14f54d659 --- /dev/null +++ b/components/lua_ui/textedit.hpp @@ -0,0 +1,18 @@ +#ifndef OPENMW_LUAUI_TEXTEDIT +#define OPENMW_LUAUI_TEXTEDIT + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaTextEdit : public MyGUI::EditBox, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaTextEdit) + + virtual void setProperties(sol::object) override; + }; +} + +#endif // OPENMW_LUAUI_TEXTEDIT diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp new file mode 100644 index 0000000000..6d7bb5063c --- /dev/null +++ b/components/lua_ui/widget.cpp @@ -0,0 +1,264 @@ +#include "widget.hpp" + +#include +#include + +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" + +namespace LuaUi +{ + WidgetExtension::WidgetExtension() + : mForcedCoord() + , mAbsoluteCoord() + , mRelativeCoord() + , mAnchor() + , mLua{ nullptr } + , mWidget{ nullptr } + , mLayout{ sol::nil } + {} + + void WidgetExtension::triggerEvent(std::string_view name, const sol::object& argument = sol::nil) const + { + auto it = mCallbacks.find(name); + if (it != mCallbacks.end()) + it->second(argument, mLayout); + } + + void WidgetExtension::create(lua_State* lua, MyGUI::Widget* self) + { + mLua = lua; + mWidget = self; + + mWidget->eventChangeCoord += MyGUI::newDelegate(this, &WidgetExtension::updateChildrenCoord); + + initialize(); + } + + void WidgetExtension::initialize() + { + // \todo might be more efficient to only register these if there are Lua callbacks + mWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::keyPress); + mWidget->eventKeyButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::keyRelease); + mWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &WidgetExtension::mouseClick); + mWidget->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WidgetExtension::mouseDoubleClick); + mWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &WidgetExtension::mousePress); + mWidget->eventMouseButtonReleased += MyGUI::newDelegate(this, &WidgetExtension::mouseRelease); + mWidget->eventMouseMove += MyGUI::newDelegate(this, &WidgetExtension::mouseMove); + mWidget->eventMouseDrag += MyGUI::newDelegate(this, &WidgetExtension::mouseDrag); + + mWidget->eventMouseSetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + mWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + mWidget->eventKeySetFocus += MyGUI::newDelegate(this, &WidgetExtension::focusGain); + mWidget->eventKeyLostFocus += MyGUI::newDelegate(this, &WidgetExtension::focusLoss); + } + + void WidgetExtension::destroy() + { + clearCallbacks(); + deinitialize(); + + for (WidgetExtension* child : mContent) + child->destroy(); + } + + void WidgetExtension::deinitialize() + { + mWidget->eventKeyButtonPressed.clear(); + mWidget->eventKeyButtonReleased.clear(); + mWidget->eventMouseButtonClick.clear(); + mWidget->eventMouseButtonDoubleClick.clear(); + mWidget->eventMouseButtonPressed.clear(); + mWidget->eventMouseButtonReleased.clear(); + mWidget->eventMouseMove.clear(); + mWidget->eventMouseDrag.m_event.clear(); + + mWidget->eventMouseSetFocus.clear(); + mWidget->eventMouseLostFocus.clear(); + mWidget->eventKeySetFocus.clear(); + mWidget->eventKeyLostFocus.clear(); + } + + sol::table WidgetExtension::makeTable() const + { + return sol::table(mLua, sol::create); + } + + sol::object WidgetExtension::keyEvent(MyGUI::KeyCode code) const + { + SDL_Keysym keySym; + keySym.sym = SDLUtil::myGuiKeyToSdl(code); + keySym.scancode = SDL_GetScancodeFromKey(keySym.sym); + keySym.mod = SDL_GetModState(); + return sol::make_object(mLua, keySym); + } + + sol::object WidgetExtension::mouseEvent(int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const + { + auto position = osg::Vec2f(left, top); + auto absolutePosition = mWidget->getAbsolutePosition(); + auto offset = position - osg::Vec2f(absolutePosition.left, absolutePosition.top); + sol::table table = makeTable(); + table["position"] = position; + table["offset"] = offset; + table["button"] = SDLUtil::myGuiMouseButtonToSdl(button); + return table; + } + + void WidgetExtension::addChild(WidgetExtension* ext) + { + mContent.push_back(ext); + } + + WidgetExtension* WidgetExtension::childAt(size_t index) const + { + return mContent.at(index); + } + + void WidgetExtension::assignChild(size_t index, WidgetExtension* ext) + { + if (mContent.size() <= index) + throw std::logic_error("Invalid widget child index"); + mContent[index] = ext; + } + + WidgetExtension* WidgetExtension::eraseChild(size_t index) + { + if (mContent.size() <= index) + throw std::logic_error("Invalid widget child index"); + auto it = mContent.begin() + index; + WidgetExtension* ext = *it; + mContent.erase(it); + return ext; + } + + void WidgetExtension::setCallback(const std::string& name, const LuaUtil::Callback& callback) + { + mCallbacks[name] = callback; + } + + void WidgetExtension::clearCallbacks() + { + mCallbacks.clear(); + } + + MyGUI::IntCoord WidgetExtension::forcedCoord() + { + return mForcedCoord; + } + + void WidgetExtension::setForcedCoord(const MyGUI::IntCoord& offset) + { + mForcedCoord = offset; + } + + void WidgetExtension::updateCoord() + { + mWidget->setCoord(calculateCoord()); + } + + void WidgetExtension::setProperties(sol::object props) + { + mAbsoluteCoord = parseProperty(props, "position", MyGUI::IntPoint()); + mAbsoluteCoord = parseProperty(props, "size", MyGUI::IntSize()); + mRelativeCoord = parseProperty(props, "relativePosition", MyGUI::FloatPoint()); + mRelativeCoord = parseProperty(props, "relativeSize", MyGUI::FloatSize()); + mAnchor = parseProperty(props, "anchor", MyGUI::FloatSize()); + mWidget->setVisible(parseProperty(props, "visible", true)); + + updateCoord(); + } + + void WidgetExtension::updateChildrenCoord(MyGUI::Widget* _widget) + { + for (auto& child : mContent) + child->updateCoord(); + } + + MyGUI::IntSize WidgetExtension::calculateSize() + { + const MyGUI::IntSize& parentSize = mWidget->getParentSize(); + MyGUI::IntSize newSize; + newSize = mAbsoluteCoord.size() + mForcedCoord.size(); + newSize.width += mRelativeCoord.width * parentSize.width; + newSize.height += mRelativeCoord.height * parentSize.height; + return newSize; + } + + MyGUI::IntPoint WidgetExtension::calculatePosition(const MyGUI::IntSize& size) + { + const MyGUI::IntSize& parentSize = mWidget->getParentSize(); + MyGUI::IntPoint newPosition; + newPosition = mAbsoluteCoord.point() + mForcedCoord.point(); + newPosition.left += mRelativeCoord.left * parentSize.width - mAnchor.width * size.width; + newPosition.top += mRelativeCoord.top * parentSize.height - mAnchor.height * size.height; + return newPosition; + } + + MyGUI::IntCoord WidgetExtension::calculateCoord() + { + MyGUI::IntCoord newCoord; + newCoord = calculateSize(); + newCoord = calculatePosition(newCoord.size()); + return newCoord; + } + + void WidgetExtension::keyPress(MyGUI::Widget*, MyGUI::KeyCode code, MyGUI::Char ch) + { + if (code == MyGUI::KeyCode::None) + { + // \todo decide how to handle unicode strings in Lua + MyGUI::UString uString; + uString.push_back(static_cast(ch)); + triggerEvent("textInput", sol::make_object(mLua, uString.asUTF8())); + } + else + triggerEvent("keyPress", keyEvent(code)); + } + + void WidgetExtension::keyRelease(MyGUI::Widget*, MyGUI::KeyCode code) + { + triggerEvent("keyRelease", keyEvent(code)); + } + + void WidgetExtension::mouseMove(MyGUI::Widget*, int left, int top) + { + triggerEvent("mouseMove", mouseEvent(left, top)); + } + + void WidgetExtension::mouseDrag(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mouseMove", mouseEvent(left, top, button)); + } + + void WidgetExtension::mouseClick(MyGUI::Widget* _widget) + { + triggerEvent("mouseClick"); + } + + void WidgetExtension::mouseDoubleClick(MyGUI::Widget* _widget) + { + triggerEvent("mouseDoubleClick"); + } + + void WidgetExtension::mousePress(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mousePress", mouseEvent(left, top, button)); + } + + void WidgetExtension::mouseRelease(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) + { + triggerEvent("mouseRelease", mouseEvent(left, top, button)); + } + + void WidgetExtension::focusGain(MyGUI::Widget*, MyGUI::Widget*) + { + triggerEvent("focusGain"); + } + + void WidgetExtension::focusLoss(MyGUI::Widget*, MyGUI::Widget*) + { + triggerEvent("focusLoss"); + } +} diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp new file mode 100644 index 0000000000..99d430f71c --- /dev/null +++ b/components/lua_ui/widget.hpp @@ -0,0 +1,99 @@ +#ifndef OPENMW_LUAUI_WIDGET +#define OPENMW_LUAUI_WIDGET + +#include + +#include +#include + +#include + +#include "properties.hpp" + +namespace LuaUi +{ + /* + * extends MyGUI::Widget and its child classes + * memory ownership is controlled by MyGUI + * it is important not to call any WidgetExtension methods after destroying the MyGUI::Widget + */ + class WidgetExtension + { + public: + WidgetExtension(); + // must be called after creating the underlying MyGUI::Widget + void create(lua_State* lua, MyGUI::Widget* self); + // must be called after before destroying the underlying MyGUI::Widget + void destroy(); + + void addChild(WidgetExtension* ext); + WidgetExtension* childAt(size_t index) const; + void assignChild(size_t index, WidgetExtension* ext); + WidgetExtension* eraseChild(size_t index); + size_t childCount() const { return mContent.size(); } + + MyGUI::Widget* widget() const { return mWidget; } + + void setCallback(const std::string&, const LuaUtil::Callback&); + void clearCallbacks(); + + virtual void setProperties(sol::object); + + MyGUI::IntCoord forcedCoord(); + void setForcedCoord(const MyGUI::IntCoord& offset); + void updateCoord(); + + void setLayout(const sol::table& layout) { mLayout = layout; } + + protected: + sol::table makeTable() const; + sol::object keyEvent(MyGUI::KeyCode) const; + sol::object mouseEvent(int left, int top, MyGUI::MouseButton button) const; + virtual void initialize(); + virtual void deinitialize(); + virtual MyGUI::IntSize calculateSize(); + virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size); + MyGUI::IntCoord calculateCoord(); + + void triggerEvent(std::string_view name, const sol::object& argument) const; + + // offsets the position and size, used only in C++ widget code + MyGUI::IntCoord mForcedCoord; + // position and size in pixels + MyGUI::IntCoord mAbsoluteCoord; + // position and size as a ratio of parent size + MyGUI::FloatCoord mRelativeCoord; + // negative position offset as a ratio of this widget's size + // used in combination with relative coord to align the widget, e. g. center it + MyGUI::FloatSize mAnchor; + + private: + // use lua_State* instead of sol::state_view because MyGUI requires a default constructor + lua_State* mLua; + MyGUI::Widget* mWidget; + + std::vector mContent; + std::map> mCallbacks; + sol::table mLayout; + + void updateChildrenCoord(MyGUI::Widget*); + + void keyPress(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char); + void keyRelease(MyGUI::Widget*, MyGUI::KeyCode); + void mouseMove(MyGUI::Widget*, int, int); + void mouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseClick(MyGUI::Widget*); + void mouseDoubleClick(MyGUI::Widget*); + void mousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void mouseRelease(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void focusGain(MyGUI::Widget*, MyGUI::Widget*); + void focusLoss(MyGUI::Widget*, MyGUI::Widget*); + }; + + class LuaWidget : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWidget) + }; +} + +#endif // !OPENMW_LUAUI_WIDGET diff --git a/components/lua_ui/widgetlist.cpp b/components/lua_ui/widgetlist.cpp new file mode 100644 index 0000000000..c2a9bef990 --- /dev/null +++ b/components/lua_ui/widgetlist.cpp @@ -0,0 +1,31 @@ +#include "widgetlist.hpp" + +#include + +#include "widget.hpp" +#include "text.hpp" +#include "textedit.hpp" +#include "window.hpp" + +namespace LuaUi +{ + + void registerAllWidgets() + { + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + MyGUI::FactoryManager::getInstance().registerFactory("Widget"); + } + + const std::unordered_map& widgetTypeToName() + { + static std::unordered_map types{ + { "LuaWidget", "Widget" }, + { "LuaText", "Text" }, + { "LuaTextEdit", "TextEdit" }, + { "LuaWindow", "Window" }, + }; + return types; + } +} diff --git a/components/lua_ui/widgetlist.hpp b/components/lua_ui/widgetlist.hpp new file mode 100644 index 0000000000..ff033fb6ca --- /dev/null +++ b/components/lua_ui/widgetlist.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_LUAUI_WIDGETLIST +#define OPENMW_LUAUI_WIDGETLIST + +#include +#include + +namespace LuaUi +{ + void registerAllWidgets(); + + const std::unordered_map& widgetTypeToName(); +} + +#endif // OPENMW_LUAUI_WIDGETLIST diff --git a/components/lua_ui/window.cpp b/components/lua_ui/window.cpp new file mode 100644 index 0000000000..5d34bf8212 --- /dev/null +++ b/components/lua_ui/window.cpp @@ -0,0 +1,96 @@ +#include "window.hpp" + +#include + +namespace LuaUi +{ + LuaWindow::LuaWindow() + : mCaption() + , mPreviousMouse() + , mChangeScale() + , mMoveResize() + {} + + void LuaWindow::initialize() + { + WidgetExtension::initialize(); + + assignWidget(mCaption, "Caption"); + if (mCaption) + { + mCaption->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress); + mCaption->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag); + } + for (auto w : getSkinWidgetsByName("Action")) + { + w->eventMouseButtonPressed += MyGUI::newDelegate(this, &LuaWindow::notifyMousePress); + w->eventMouseDrag += MyGUI::newDelegate(this, &LuaWindow::notifyMouseDrag); + } + } + + void LuaWindow::deinitialize() + { + WidgetExtension::deinitialize(); + + if (mCaption) + { + mCaption->eventMouseButtonPressed.clear(); + mCaption->eventMouseDrag.m_event.clear(); + } + for (auto w : getSkinWidgetsByName("Action")) + { + w->eventMouseButtonPressed.clear(); + w->eventMouseDrag.m_event.clear(); + } + } + + void LuaWindow::setProperties(sol::object props) + { + if (mCaption) + mCaption->setCaption(parseProperty(props, "caption", std::string())); + mMoveResize = MyGUI::IntCoord(); + setForcedCoord(mMoveResize); + WidgetExtension::setProperties(props); + } + + void LuaWindow::notifyMousePress(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + if (sender->isUserString("Scale")) + mChangeScale = MyGUI::IntCoord::parse(sender->getUserString("Scale")); + else + mChangeScale = MyGUI::IntCoord(1, 1, 0, 0); + } + + void LuaWindow::notifyMouseDrag(MyGUI::Widget* sender, int left, int top, MyGUI::MouseButton id) + { + if (id != MyGUI::MouseButton::Left) + return; + + MyGUI::IntCoord change = mChangeScale; + change.left *= (left - mPreviousMouse.left); + change.top *= (top - mPreviousMouse.top); + change.width *= (left - mPreviousMouse.left); + change.height *= (top - mPreviousMouse.top); + + mMoveResize = mMoveResize + change.size(); + setForcedCoord(mMoveResize); + // position can change based on size changes + mMoveResize = mMoveResize + change.point() + getPosition() - calculateCoord().point(); + setForcedCoord(mMoveResize); + updateCoord(); + + mPreviousMouse.left = left; + mPreviousMouse.top = top; + + sol::table table = makeTable(); + table["position"] = osg::Vec2f(mCoord.left, mCoord.top); + table["size"] = osg::Vec2f(mCoord.width, mCoord.height); + triggerEvent("windowDrag", table); + } +} diff --git a/components/lua_ui/window.hpp b/components/lua_ui/window.hpp new file mode 100644 index 0000000000..f34779c8f7 --- /dev/null +++ b/components/lua_ui/window.hpp @@ -0,0 +1,37 @@ +#ifndef OPENMW_LUAUI_WINDOW +#define OPENMW_LUAUI_WINDOW + +#include + +#include + +#include "widget.hpp" + +namespace LuaUi +{ + class LuaWindow : public MyGUI::Widget, public WidgetExtension + { + MYGUI_RTTI_DERIVED(LuaWindow) + + public: + LuaWindow(); + virtual void setProperties(sol::object) override; + + private: + // \todo replace with LuaText when skins are properly implemented + MyGUI::TextBox* mCaption; + MyGUI::IntPoint mPreviousMouse; + MyGUI::IntCoord mChangeScale; + + MyGUI::IntCoord mMoveResize; + + protected: + virtual void initialize() override; + virtual void deinitialize() override; + + void notifyMousePress(MyGUI::Widget*, int, int, MyGUI::MouseButton); + void notifyMouseDrag(MyGUI::Widget*, int, int, MyGUI::MouseButton); + }; +} + +#endif // OPENMW_LUAUI_WINDOW diff --git a/components/misc/algorithm.hpp b/components/misc/algorithm.hpp index 4d70afa86c..54ac74e97e 100644 --- a/components/misc/algorithm.hpp +++ b/components/misc/algorithm.hpp @@ -4,6 +4,8 @@ #include #include +#include "stringops.hpp" + namespace Misc { template @@ -31,6 +33,30 @@ namespace Misc } return begin; } + + /// Performs a binary search on a sorted container for a string that 'key' starts with + template + static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + { + const Iterator notFound = end; + + while(begin < end) + { + const Iterator middle = begin + (std::distance(begin, end) / 2); + + int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); + + if(comp == 0) + return middle; + else if(comp > 0) + end = middle; + else + begin = middle + 1; + } + + return notFound; + } + } #endif diff --git a/components/misc/compression.cpp b/components/misc/compression.cpp new file mode 100644 index 0000000000..7f76d0900c --- /dev/null +++ b/components/misc/compression.cpp @@ -0,0 +1,48 @@ +#include "compression.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace Misc +{ + std::vector compress(const std::vector& data) + { + const std::size_t originalSize = data.size(); + std::vector result(static_cast(LZ4_compressBound(static_cast(originalSize)) + sizeof(originalSize))); + const int size = LZ4_compress_default( + reinterpret_cast(data.data()), + reinterpret_cast(result.data()) + sizeof(originalSize), + static_cast(data.size()), + static_cast(result.size() - sizeof(originalSize)) + ); + if (size == 0) + throw std::runtime_error("Failed to compress"); + std::memcpy(result.data(), &originalSize, sizeof(originalSize)); + result.resize(static_cast(size) + sizeof(originalSize)); + return result; + } + + std::vector decompress(const std::vector& data) + { + std::size_t originalSize; + std::memcpy(&originalSize, data.data(), sizeof(originalSize)); + std::vector result(originalSize); + const int size = LZ4_decompress_safe( + reinterpret_cast(data.data()) + sizeof(originalSize), + reinterpret_cast(result.data()), + static_cast(data.size() - sizeof(originalSize)), + static_cast(result.size()) + ); + if (size < 0) + throw std::runtime_error("Failed to decompress"); + if (originalSize != static_cast(size)) + throw std::runtime_error("Size of decompressed data (" + std::to_string(size) + + ") doesn't match stored (" + std::to_string(originalSize) + ")"); + return result; + } +} diff --git a/components/misc/compression.hpp b/components/misc/compression.hpp new file mode 100644 index 0000000000..2b951ebed6 --- /dev/null +++ b/components/misc/compression.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_MISC_COMPRESSION_H +#define OPENMW_COMPONENTS_MISC_COMPRESSION_H + +#include +#include + +namespace Misc +{ + std::vector compress(const std::vector& data); + + std::vector decompress(const std::vector& data); +} + +#endif diff --git a/components/misc/constants.hpp b/components/misc/constants.hpp index bfd3933fc7..01d783a4fc 100644 --- a/components/misc/constants.hpp +++ b/components/misc/constants.hpp @@ -36,6 +36,9 @@ const std::string HerbalismLabel = "HerbalismSwitch"; // Percentage height at which projectiles are spawned from an actor const float TorsoHeight = 0.75f; +static constexpr float sStepSizeUp = 34.0f; +static constexpr float sMaxSlope = 46.0f; + } #endif diff --git a/components/misc/convert.hpp b/components/misc/convert.hpp index c5784d33ae..6f4a55cfcc 100644 --- a/components/misc/convert.hpp +++ b/components/misc/convert.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_COMPONENTS_MISC_CONVERT_H #define OPENMW_COMPONENTS_MISC_CONVERT_H +#include #include #include @@ -18,11 +19,6 @@ namespace Convert return osg::Vec3f(values[0], values[1], values[2]); } - inline osg::Vec3f makeOsgVec3f(const btVector3& value) - { - return osg::Vec3f(value.x(), value.y(), value.z()); - } - inline osg::Vec3f makeOsgVec3f(const ESM::Pathgrid::Point& value) { return osg::Vec3f(value.mX, value.mY, value.mZ); @@ -47,6 +43,30 @@ namespace Convert { return osg::Quat(quat.x(), quat.y(), quat.z(), quat.w()); } + + inline osg::Quat makeOsgQuat(const float (&rotation)[3]) + { + return osg::Quat(rotation[2], osg::Vec3f(0, 0, -1)) + * osg::Quat(rotation[1], osg::Vec3f(0, -1, 0)) + * osg::Quat(rotation[0], osg::Vec3f(-1, 0, 0)); + } + + inline osg::Quat makeOsgQuat(const ESM::Position& position) + { + return makeOsgQuat(position.rot); + } + + inline btQuaternion makeBulletQuaternion(const float (&rotation)[3]) + { + return btQuaternion(btVector3(0, 0, -1), rotation[2]) + * btQuaternion(btVector3(0, -1, 0), rotation[1]) + * btQuaternion(btVector3(-1, 0, 0), rotation[0]); + } + + inline btQuaternion makeBulletQuaternion(const ESM::Position& position) + { + return makeBulletQuaternion(position.rot); + } } } diff --git a/components/misc/errorMarker.cpp b/components/misc/errorMarker.cpp new file mode 100644 index 0000000000..fd2b0b53ac --- /dev/null +++ b/components/misc/errorMarker.cpp @@ -0,0 +1,1390 @@ +#include "errorMarker.hpp" + +namespace Misc +{ + const std::string errorMarker = "#Ascii Scene " +"#Version 162 " +"#Generator OpenSceneGraph 3.6.5 " +"" +"osg::Group {" +" UniqueID 1 " +" Children 5 {" +" osg::Geode {" +" UniqueID 2 " +" Name \"Error\" " +" Drawables 1 {" +" osg::Geometry {" +" UniqueID 3 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 4 " +" DataVariance STATIC " +" ModeList 1 {" +" GL_BLEND ON " +" }" +" AttributeList 1 {" +" osg::Material {" +" UniqueID 5 " +" Name \"Error\" " +" Ambient TRUE Front 1 1 1 0.5 Back 1 1 1 0.5 " +" Diffuse TRUE Front 0.8 0.704 0.32 0.5 Back 0.8 0.704 0.32 0.5 " +" Specular TRUE Front 0.5 0.5 0.5 0.5 Back 0.5 0.5 0.5 0.5 " +" Emission TRUE Front 1 0.88 0.4 0.5 Back 1 0.88 0.4 0.5 " +" Shininess TRUE Front 28.8 Back 28.8 " +" }" +" Value OFF " +" }" +" RenderingHint 2 " +" RenderBinMode USE_RENDERBIN_DETAILS " +" BinNumber 10 " +" BinName \"DepthSortedBin\" " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 6 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 7 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 108 {" +" 0 1 2 3 " +" 0 2 2 4 " +" 3 5 3 4 " +" 4 6 5 6 " +" 7 8 8 9 " +" 10 6 8 10 " +" 5 6 11 11 " +" 6 10 12 5 " +" 11 13 12 11 " +" 11 14 13 10 " +" 15 11 11 15 " +" 16 15 17 16 " +" 18 16 17 17 " +" 19 18 20 21 " +" 22 23 20 22 " +" 22 24 23 25 " +" 23 24 24 26 " +" 25 26 27 28 " +" 28 29 30 26 " +" 28 30 25 26 " +" 31 31 26 30 " +" 32 25 31 33 " +" 32 31 31 34 " +" 33 30 35 31 " +" 31 35 36 35 " +" 37 36 38 36 " +" 37 37 39 38 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 8 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" -4.51996 -5.9634 -60.7026 " +" 2e-06 -5.96339 -61.6017 " +" 4.51996 -5.96339 -60.7026 " +" -8.3518 -5.9634 -58.1422 " +" 8.3518 -5.96339 -58.1422 " +" -58.1422 -5.96341 -8.3518 " +" 58.1422 -5.96339 -8.3518 " +" 60.7026 -5.96339 -4.51996 " +" 61.6017 -5.96339 0 " +" 60.7026 -5.96339 4.51996 " +" 58.1423 -5.96339 8.3518 " +" -58.1422 -5.96341 8.3518 " +" -60.7026 -5.96341 -4.51996 " +" -61.6017 -5.96341 0 " +" -60.7026 -5.96341 4.51996 " +" 8.3518 -5.9634 58.1422 " +" -8.3518 -5.96341 58.1422 " +" 4.51997 -5.96341 60.7026 " +" -4.51996 -5.96341 60.7026 " +" 2e-06 -5.96341 61.6017 " +" -60.7026 5.96339 -4.51996 " +" -61.6017 5.96339 0 " +" -60.7026 5.96339 4.51996 " +" -58.1423 5.96339 -8.3518 " +" -58.1422 5.96339 8.3518 " +" -8.3518 5.9634 -58.1422 " +" -8.3518 5.96339 58.1422 " +" -4.51996 5.96339 60.7026 " +" -2e-06 5.96339 61.6017 " +" 4.51996 5.9634 60.7026 " +" 8.3518 5.9634 58.1422 " +" 8.3518 5.96341 -58.1422 " +" -4.51997 5.96341 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" 4.51996 5.96341 -60.7026 " +" 58.1422 5.96341 8.3518 " +" 58.1422 5.96341 -8.3518 " +" 60.7026 5.96341 4.51996 " +" 60.7026 5.96341 -4.51996 " +" 61.6017 5.96341 0 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 10 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 11 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 9 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 40 {" +" 0.37739 0.519384 " +" 0.384197 0.509197 " +" 0.394384 0.50239 " +" 0.375 0.531401 " +" 0.406401 0.5 " +" 0.375 0.718599 " +" 0.593599 0.5 " +" 0.605616 0.50239 " +" 0.615803 0.509197 " +" 0.62261 0.519384 " +" 0.625 0.531401 " +" 0.406401 0.75 " +" 0.37739 0.730616 " +" 0.384197 0.740803 " +" 0.394384 0.74761 " +" 0.625 0.718599 " +" 0.593599 0.75 " +" 0.62261 0.730616 " +" 0.605616 0.74761 " +" 0.615803 0.740803 " +" 0.37739 0.019384 " +" 0.384197 0.009197 " +" 0.394384 0.00239 " +" 0.375 0.031401 " +" 0.406401 0 " +" 0.375 0.218599 " +" 0.593599 0 " +" 0.605616 0.00239 " +" 0.615803 0.009197 " +" 0.62261 0.019384 " +" 0.625 0.031401 " +" 0.406401 0.25 " +" 0.37739 0.230616 " +" 0.384197 0.240803 " +" 0.394384 0.24761 " +" 0.625 0.218599 " +" 0.593599 0.25 " +" 0.62261 0.230616 " +" 0.605616 0.24761 " +" 0.615803 0.240803 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Geode {" +" UniqueID 12 " +" Name \"Error\" " +" Drawables 1 {" +" osg::Geometry {" +" UniqueID 13 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 4 " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 14 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 15 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 120 {" +" 0 1 2 0 " +" 3 1 3 4 " +" 1 3 5 4 " +" 4 5 6 4 " +" 6 7 8 7 " +" 6 8 6 9 " +" 10 8 9 10 " +" 9 11 12 13 " +" 11 12 11 14 " +" 15 12 14 15 " +" 14 16 16 17 " +" 15 16 18 17 " +" 18 19 17 18 " +" 20 19 20 21 " +" 19 20 22 21 " +" 22 23 24 22 " +" 25 23 25 26 " +" 23 25 27 26 " +" 28 26 27 28 " +" 29 26 30 29 " +" 28 30 28 31 " +" 32 30 31 32 " +" 31 33 34 35 " +" 33 34 33 36 " +" 37 34 36 37 " +" 36 38 39 37 " +" 38 39 38 40 " +" 40 41 39 40 " +" 42 41 42 43 " +" 41 42 0 43 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 16 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 61.6017 -5.96339 0 " +" 60.7026 5.96341 -4.51996 " +" 61.6017 5.96341 0 " +" 60.7026 -5.96339 -4.51996 " +" 58.1422 5.96341 -8.3518 " +" 58.1422 -5.96339 -8.3518 " +" 8.3518 -5.96339 -58.1422 " +" 8.3518 5.96341 -58.1422 " +" 4.51996 5.96341 -60.7026 " +" 4.51996 -5.96339 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" 2e-06 -5.96339 -61.6017 " +" -4.51997 5.96341 -60.7026 " +" -2e-06 5.96341 -61.6017 " +" -4.51996 -5.9634 -60.7026 " +" -8.3518 5.9634 -58.1422 " +" -8.3518 -5.9634 -58.1422 " +" -58.1423 5.96339 -8.3518 " +" -58.1422 -5.96341 -8.3518 " +" -60.7026 5.96339 -4.51996 " +" -60.7026 -5.96341 -4.51996 " +" -61.6017 5.96339 0 " +" -61.6017 -5.96341 0 " +" -60.7026 5.96339 4.51996 " +" -61.6017 5.96339 0 " +" -60.7026 -5.96341 4.51996 " +" -58.1422 5.96339 8.3518 " +" -58.1422 -5.96341 8.3518 " +" -8.3518 -5.96341 58.1422 " +" -8.3518 5.96339 58.1422 " +" -4.51996 5.96339 60.7026 " +" -4.51996 -5.96341 60.7026 " +" -2e-06 5.96339 61.6017 " +" 2e-06 -5.96341 61.6017 " +" 4.51996 5.9634 60.7026 " +" -2e-06 5.96339 61.6017 " +" 4.51997 -5.96341 60.7026 " +" 8.3518 5.9634 58.1422 " +" 8.3518 -5.9634 58.1422 " +" 58.1422 5.96341 8.3518 " +" 58.1423 -5.96339 8.3518 " +" 60.7026 5.96341 4.51996 " +" 60.7026 -5.96339 4.51996 " +" 61.6017 5.96341 0 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 18 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 1 0 0 " +" 0.923877 0 -0.38269 " +" 0.980784 0 -0.195097 " +" 0.923877 0 -0.38269 " +" 0.773003 0 -0.634402 " +" 0.773003 0 -0.634402 " +" 0.634402 0 -0.773003 " +" 0.634402 0 -0.773003 " +" 0.38269 0 -0.923877 " +" 0.38269 0 -0.923877 " +" 0.195097 0 -0.980784 " +" 0 0 -1 " +" -0.38269 -0 -0.923877 " +" -0.195097 -0 -0.980784 " +" -0.38269 -0 -0.923877 " +" -0.634402 -0 -0.773003 " +" -0.634402 -0 -0.773003 " +" -0.773003 -0 -0.634402 " +" -0.773003 -0 -0.634402 " +" -0.923877 -0 -0.38269 " +" -0.923877 -0 -0.38269 " +" -0.980784 -0 -0.195097 " +" -1 0 0 " +" -0.923877 0 0.38269 " +" -0.980784 0 0.195097 " +" -0.923877 0 0.38269 " +" -0.773003 0 0.634402 " +" -0.773003 0 0.634402 " +" -0.634402 0 0.773003 " +" -0.634402 0 0.773003 " +" -0.38269 0 0.923877 " +" -0.38269 0 0.923877 " +" -0.195097 0 0.980784 " +" 0 0 1 " +" 0.38269 0 0.923877 " +" 0.195097 0 0.980784 " +" 0.38269 0 0.923877 " +" 0.634402 0 0.773003 " +" 0.634402 0 0.773003 " +" 0.773003 0 0.634402 " +" 0.773003 0 0.634402 " +" 0.923877 0 0.38269 " +" 0.923877 0 0.38269 " +" 0.980784 0 0.195097 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 19 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 17 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 44 {" +" 0.625 0.5 " +" 0.605616 0.25 " +" 0.625 0.25 " +" 0.605616 0.5 " +" 0.593599 0.25 " +" 0.593599 0.5 " +" 0.406401 0.5 " +" 0.406401 0.25 " +" 0.394384 0.25 " +" 0.394384 0.5 " +" 0.375 0.25 " +" 0.375 0.5 " +" 0.125 0.519384 " +" 0.125 0.5 " +" 0.375 0.519384 " +" 0.125 0.531401 " +" 0.375 0.531401 " +" 0.125 0.718599 " +" 0.375 0.718599 " +" 0.125 0.730616 " +" 0.375 0.730616 " +" 0.125 0.75 " +" 0.375 0.75 " +" 0.394384 1 " +" 0.375 1 " +" 0.394384 0.75 " +" 0.406401 1 " +" 0.406401 0.75 " +" 0.593599 0.75 " +" 0.593599 1 " +" 0.605616 1 " +" 0.605616 0.75 " +" 0.625 1 " +" 0.625 0.75 " +" 0.875 0.730616 " +" 0.875 0.75 " +" 0.625 0.730616 " +" 0.875 0.718599 " +" 0.625 0.718599 " +" 0.875 0.531401 " +" 0.625 0.531401 " +" 0.875 0.519384 " +" 0.625 0.519384 " +" 0.875 0.5 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Geode {" +" UniqueID 20 " +" Name \"Error\" " +" Drawables 1 {" +" osg::Geometry {" +" UniqueID 21 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 22 " +" DataVariance STATIC " +" AttributeList 1 {" +" osg::Material {" +" UniqueID 23 " +" Name \"ErrorLabel\" " +" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 " +" Diffuse TRUE Front 0.176208 0.176208 0.176208 1 Back 0.176208 0.176208 0.176208 1 " +" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 " +" Emission TRUE Front 0.22026 0.22026 0.22026 1 Back 0.22026 0.22026 0.22026 1 " +" Shininess TRUE Front 28.8 Back 28.8 " +" }" +" Value OFF " +" }" +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 24 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 25 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 216 {" +" 0 1 2 3 " +" 0 2 2 4 " +" 3 4 5 3 " +" 5 6 7 5 " +" 8 3 8 5 " +" 7 7 9 8 " +" 10 3 8 8 " +" 11 10 12 13 " +" 10 14 12 10 " +" 15 14 10 10 " +" 11 15 16 15 " +" 11 11 17 16 " +" 18 16 17 17 " +" 19 18 20 21 " +" 22 23 20 22 " +" 22 24 23 25 " +" 23 24 24 26 " +" 25 26 27 28 " +" 28 29 30 26 " +" 28 30 25 26 " +" 31 31 26 30 " +" 32 25 31 33 " +" 32 31 31 34 " +" 33 30 35 31 " +" 31 35 36 35 " +" 37 36 38 36 " +" 37 37 39 38 " +" 40 41 42 43 " +" 40 42 42 44 " +" 43 45 43 44 " +" 44 46 45 47 " +" 45 46 48 47 " +" 46 46 49 48 " +" 44 50 46 51 " +" 46 50 50 52 " +" 53 54 50 53 " +" 53 55 54 50 " +" 54 51 54 56 " +" 51 56 57 51 " +" 58 51 57 57 " +" 59 58 60 61 " +" 62 63 60 62 " +" 62 64 63 65 " +" 63 64 64 66 " +" 65 66 67 68 " +" 69 70 65 71 " +" 69 65 72 71 " +" 65 66 68 73 " +" 65 66 73 68 " +" 74 73 73 72 " +" 65 73 75 72 " +" 72 75 76 75 " +" 77 76 78 76 " +" 77 77 79 78 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 26 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -7.12646 -9.95049 -13.7349 " +" -6.35115 -9.95049 -14.8952 " +" -5.1908 -9.95049 -15.6705 " +" -7.39872 -9.95049 -12.3661 " +" -3.82208 -9.95049 -15.9428 " +" 3.82208 -9.95049 -15.9428 " +" 5.1908 -9.95049 -15.6705 " +" 6.35115 -9.95049 -14.8952 " +" 7.39872 -9.95049 -12.3661 " +" 7.12646 -9.95049 -13.7349 " +" -7.39872 -9.95049 33.4208 " +" 7.39872 -9.95049 33.4208 " +" -6.35115 -9.95049 35.9499 " +" -7.12647 -9.95049 34.7895 " +" -5.1908 -9.95049 36.7252 " +" -3.82208 -9.95049 36.9975 " +" 3.82208 -9.95049 36.9975 " +" 7.12646 -9.95049 34.7895 " +" 5.1908 -9.95049 36.7252 " +" 6.35115 -9.95049 35.9499 " +" -7.12646 -9.95042 -36.7346 " +" -6.35115 -9.95042 -37.8949 " +" -5.1908 -9.95042 -38.6702 " +" -7.39872 -9.95042 -35.3659 " +" -3.82208 -9.95042 -38.9425 " +" -7.39872 -9.95042 -27.7217 " +" 3.82208 -9.95042 -38.9425 " +" 5.1908 -9.95042 -38.6702 " +" 6.35115 -9.95042 -37.8949 " +" 7.12646 -9.95042 -36.7346 " +" 7.39872 -9.95042 -35.3659 " +" -3.82208 -9.95042 -24.1451 " +" -7.12647 -9.95042 -26.353 " +" -6.35115 -9.95042 -25.1926 " +" -5.1908 -9.95042 -24.4173 " +" 7.39872 -9.95042 -27.7217 " +" 3.82208 -9.95042 -24.1451 " +" 7.12646 -9.95042 -26.353 " +" 5.1908 -9.95042 -24.4173 " +" 6.35115 -9.95042 -25.1926 " +" -5.1908 9.95055 -15.6705 " +" -6.35115 9.95055 -14.8952 " +" -7.12646 9.95055 -13.7349 " +" -3.82208 9.95055 -15.9428 " +" -7.39872 9.95055 -12.3661 " +" 3.82208 9.95055 -15.9428 " +" 7.39872 9.95055 -12.3661 " +" 5.1908 9.95055 -15.6705 " +" 6.35115 9.95055 -14.8952 " +" 7.12646 9.95055 -13.7349 " +" -7.39872 9.95055 33.4208 " +" 7.39872 9.95055 33.4208 " +" -7.12646 9.95055 34.7895 " +" -6.35115 9.95056 35.9499 " +" -3.82208 9.95056 36.9975 " +" -5.1908 9.95056 36.7252 " +" 3.82208 9.95055 36.9975 " +" 5.19081 9.95055 36.7252 " +" 7.12646 9.95056 34.7895 " +" 6.35115 9.95055 35.9499 " +" -5.1908 9.95062 -38.6702 " +" -6.35115 9.95062 -37.8949 " +" -7.12646 9.95062 -36.7346 " +" -3.82208 9.95062 -38.9425 " +" -7.39872 9.95062 -35.3659 " +" 3.82208 9.95062 -38.9425 " +" -7.39872 9.95063 -27.7217 " +" -7.12646 9.95063 -26.353 " +" -6.35115 9.95063 -25.1926 " +" 6.35115 9.95062 -37.8949 " +" 5.1908 9.95062 -38.6702 " +" 7.12647 9.95062 -36.7346 " +" 7.39872 9.95062 -35.3659 " +" -3.82208 9.95063 -24.1451 " +" -5.1908 9.95063 -24.4173 " +" 3.82208 9.95063 -24.1451 " +" 7.39872 9.95062 -27.7217 " +" 5.1908 9.95063 -24.4173 " +" 7.12646 9.95063 -26.353 " +" 6.35115 9.95063 -25.1926 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 28 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 29 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 27 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0.006133 0.041706 " +" 0.023598 0.006596 " +" 0.149209 0.001714 " +" 0 0.06756 " +" 0.241707 0 " +" 0.758294 0 " +" 0.850791 0.001714 " +" 0.976402 0.006596 " +" 1 0.06756 " +" 0.993867 0.041706 " +" 0 0.93244 " +" 1 0.93244 " +" 0.023598 0.993404 " +" 0.006133 0.958294 " +" 0.149209 0.998286 " +" 0.241706 1 " +" 0.758294 1 " +" 0.993867 0.958294 " +" 0.850791 0.998286 " +" 0.976402 0.993404 " +" 0.006133 0.149209 " +" 0.023598 0.023598 " +" 0.149209 0.006133 " +" 0 0.241707 " +" 0.241707 0 " +" 0 0.758294 " +" 0.758294 0 " +" 0.850791 0.006133 " +" 0.976402 0.023598 " +" 0.993867 0.149209 " +" 1 0.241707 " +" 0.241706 1 " +" 0.006133 0.850791 " +" 0.023598 0.976402 " +" 0.149209 0.993867 " +" 1 0.758293 " +" 0.758294 1 " +" 0.993867 0.850791 " +" 0.850791 0.993867 " +" 0.976402 0.976402 " +" 0.149209 0.001714 " +" 0.023598 0.006596 " +" 0.006133 0.041706 " +" 0.241706 0 " +" 0 0.06756 " +" 0.758294 0 " +" 1 0.06756 " +" 0.850791 0.001714 " +" 0.976402 0.006596 " +" 0.993867 0.041706 " +" 0 0.93244 " +" 1 0.93244 " +" 0.006133 0.958294 " +" 0.023598 0.993404 " +" 0.241707 1 " +" 0.149209 0.998286 " +" 0.758294 1 " +" 0.850791 0.998286 " +" 0.993867 0.958294 " +" 0.976402 0.993404 " +" 0.149209 0.006133 " +" 0.023598 0.023598 " +" 0.006133 0.149209 " +" 0.241706 0 " +" 0 0.241707 " +" 0.758294 0 " +" 0 0.758294 " +" 0.006133 0.850791 " +" 0.023598 0.976402 " +" 0.976402 0.023598 " +" 0.850791 0.006133 " +" 0.993867 0.149209 " +" 1 0.241706 " +" 0.241707 1 " +" 0.149209 0.993867 " +" 0.758294 1 " +" 1 0.758294 " +" 0.850791 0.993867 " +" 0.993867 0.850791 " +" 0.976402 0.976402 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Geode {" +" UniqueID 30 " +" Name \"Error\" " +" Drawables 1 {" +" osg::Geometry {" +" UniqueID 31 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 22 " +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 32 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 33 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 240 {" +" 0 1 2 0 " +" 3 1 0 2 " +" 4 0 5 3 " +" 4 2 6 5 " +" 7 3 4 6 " +" 8 5 9 7 " +" 6 10 8 9 " +" 11 7 6 12 " +" 10 9 13 11 " +" 12 14 10 13 " +" 15 11 12 16 " +" 14 13 17 15 " +" 16 18 14 19 " +" 15 17 16 20 " +" 18 19 21 15 " +" 18 20 22 23 " +" 21 19 18 22 " +" 24 23 19 25 " +" 26 24 22 27 " +" 23 25 26 22 " +" 28 27 25 29 " +" 30 26 28 31 " +" 27 29 30 32 " +" 26 31 29 33 " +" 34 32 30 35 " +" 31 33 34 30 " +" 36 35 33 37 " +" 38 34 36 39 " +" 35 37 38 39 " +" 34 39 38 35 " +" 40 41 42 43 " +" 41 40 40 42 " +" 44 43 40 45 " +" 46 44 42 47 " +" 43 45 46 48 " +" 44 47 45 49 " +" 50 48 46 51 " +" 47 49 50 46 " +" 52 51 49 53 " +" 54 50 52 55 " +" 51 53 54 52 " +" 56 55 53 57 " +" 58 54 56 57 " +" 59 55 58 56 " +" 60 57 61 59 " +" 58 60 62 61 " +" 63 59 58 62 " +" 64 61 65 63 " +" 62 66 64 65 " +" 67 63 62 68 " +" 66 65 69 67 " +" 66 68 70 69 " +" 71 67 66 70 " +" 72 69 73 71 " +" 70 74 72 71 " +" 73 75 70 76 " +" 74 71 75 77 " +" 76 78 74 75 " +" 79 77 76 79 " +" 78 75 78 79 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 34 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -7.39872 9.95055 -12.3661 " +" -7.39872 -9.95049 -12.3661 " +" -7.39872 -9.95049 33.4208 " +" -7.12646 -9.95049 -13.7349 " +" -7.39872 9.95055 33.4208 " +" -7.12646 9.95055 -13.7349 " +" -7.12647 -9.95049 34.7895 " +" -6.35115 -9.95049 -14.8952 " +" -7.12646 9.95055 34.7895 " +" -6.35115 9.95055 -14.8952 " +" -6.35115 9.95056 35.9499 " +" -5.1908 -9.95049 -15.6705 " +" -6.35115 -9.95049 35.9499 " +" -5.1908 9.95055 -15.6705 " +" -5.1908 9.95056 36.7252 " +" -3.82208 -9.95049 -15.9428 " +" -5.1908 -9.95049 36.7252 " +" -3.82208 9.95055 -15.9428 " +" -3.82208 9.95056 36.9975 " +" 3.82208 9.95055 -15.9428 " +" -3.82208 -9.95049 36.9975 " +" 3.82208 -9.95049 -15.9428 " +" 3.82208 -9.95049 36.9975 " +" 5.1908 -9.95049 -15.6705 " +" 3.82208 9.95055 36.9975 " +" 5.1908 9.95055 -15.6705 " +" 5.19081 9.95055 36.7252 " +" 6.35115 -9.95049 -14.8952 " +" 5.1908 -9.95049 36.7252 " +" 6.35115 9.95055 -14.8952 " +" 6.35115 -9.95049 35.9499 " +" 7.12646 -9.95049 -13.7349 " +" 6.35115 9.95055 35.9499 " +" 7.12646 9.95055 -13.7349 " +" 7.12646 9.95056 34.7895 " +" 7.39872 -9.95049 -12.3661 " +" 7.12646 -9.95049 34.7895 " +" 7.39872 9.95055 -12.3661 " +" 7.39872 -9.95049 33.4208 " +" 7.39872 9.95055 33.4208 " +" -3.82208 9.95063 -24.1451 " +" -3.82208 -9.95042 -24.1451 " +" 3.82208 -9.95042 -24.1451 " +" -5.1908 -9.95042 -24.4173 " +" 3.82208 9.95063 -24.1451 " +" -5.1908 9.95063 -24.4173 " +" 5.1908 -9.95042 -24.4173 " +" -6.35115 -9.95042 -25.1926 " +" 5.1908 9.95063 -24.4173 " +" -6.35115 9.95063 -25.1926 " +" 6.35115 9.95063 -25.1926 " +" -7.12647 -9.95042 -26.353 " +" 6.35115 -9.95042 -25.1926 " +" -7.12646 9.95063 -26.353 " +" 7.12646 9.95063 -26.353 " +" -7.39872 -9.95042 -27.7217 " +" 7.12646 -9.95042 -26.353 " +" -7.39872 9.95063 -27.7217 " +" 7.39872 9.95062 -27.7217 " +" -7.39872 -9.95042 -35.3659 " +" 7.39872 -9.95042 -27.7217 " +" -7.39872 9.95062 -35.3659 " +" 7.39872 -9.95042 -35.3659 " +" -7.12646 -9.95042 -36.7346 " +" 7.39872 9.95062 -35.3659 " +" -7.12646 9.95062 -36.7346 " +" 7.12647 9.95062 -36.7346 " +" -6.35115 -9.95042 -37.8949 " +" 7.12646 -9.95042 -36.7346 " +" -6.35115 9.95062 -37.8949 " +" 6.35115 -9.95042 -37.8949 " +" -5.1908 -9.95042 -38.6702 " +" 6.35115 9.95062 -37.8949 " +" -5.1908 9.95062 -38.6702 " +" 5.1908 9.95062 -38.6702 " +" -3.82208 9.95062 -38.9425 " +" 5.1908 -9.95042 -38.6702 " +" -3.82208 -9.95042 -38.9425 " +" 3.82208 9.95062 -38.9425 " +" 3.82208 -9.95042 -38.9425 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 36 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" -0.995187 -0 -0.0979987 " +" -0.995187 -0 -0.0979987 " +" -0.995187 0 0.0979987 " +" -0.923877 -0 -0.38269 " +" -0.995187 0 0.0979987 " +" -0.923877 -0 -0.38269 " +" -0.923877 0 0.38269 " +" -0.707107 -0 -0.707107 " +" -0.923877 0 0.38269 " +" -0.707107 -0 -0.707107 " +" -0.707107 0 0.707107 " +" -0.38269 -0 -0.923877 " +" -0.707107 0 0.707107 " +" -0.38269 -0 -0.923877 " +" -0.38269 0 0.923877 " +" -0.0979987 -0 -0.995187 " +" -0.38269 0 0.923877 " +" -0.0979987 -0 -0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 -0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 -0.995187 " +" 0.0979987 0 0.995187 " +" 0.38269 0 -0.923877 " +" 0.0979987 0 0.995187 " +" 0.38269 0 -0.923877 " +" 0.38269 0 0.923877 " +" 0.707107 0 -0.707107 " +" 0.38269 0 0.923877 " +" 0.707107 0 -0.707107 " +" 0.707107 0 0.707107 " +" 0.923877 0 -0.38269 " +" 0.707107 0 0.707107 " +" 0.923877 0 -0.38269 " +" 0.923877 0 0.38269 " +" 0.995187 0 -0.0979987 " +" 0.923877 0 0.38269 " +" 0.995187 0 -0.0979987 " +" 0.995187 0 0.0979987 " +" 0.995187 0 0.0979987 " +" -0.0979987 0 0.995187 " +" -0.0979987 0 0.995187 " +" 0.0979987 0 0.995187 " +" -0.38269 0 0.923877 " +" 0.0979987 0 0.995187 " +" -0.38269 0 0.923877 " +" 0.38269 0 0.923877 " +" -0.707107 0 0.707107 " +" 0.38269 0 0.923877 " +" -0.707107 0 0.707107 " +" 0.707107 0 0.707107 " +" -0.923877 0 0.38269 " +" 0.707107 0 0.707107 " +" -0.923877 0 0.38269 " +" 0.923877 0 0.38269 " +" -0.995187 0 0.0979987 " +" 0.923877 0 0.38269 " +" -0.995187 0 0.0979987 " +" 0.995187 0 0.0979987 " +" -0.995187 -0 -0.0979987 " +" 0.995187 0 0.0979987 " +" -0.995187 -0 -0.0979987 " +" 0.995187 0 -0.0979987 " +" -0.923877 -0 -0.38269 " +" 0.995187 0 -0.0979987 " +" -0.923877 -0 -0.38269 " +" 0.923877 0 -0.38269 " +" -0.707107 -0 -0.707107 " +" 0.923877 0 -0.38269 " +" -0.707107 -0 -0.707107 " +" 0.707107 0 -0.707107 " +" -0.38269 -0 -0.923877 " +" 0.707107 0 -0.707107 " +" -0.38269 -0 -0.923877 " +" 0.38269 0 -0.923877 " +" -0.0979987 -0 -0.995187 " +" 0.38269 0 -0.923877 " +" -0.0979987 -0 -0.995187 " +" 0.0979987 0 -0.995187 " +" 0.0979987 0 -0.995187 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 37 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 35 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 80 {" +" 0 0.06756 " +" 0 0.06756 " +" 0 0.93244 " +" 0.006133 0.041706 " +" 0 0.93244 " +" 0.006133 0.041706 " +" 0.006133 0.958294 " +" 0.023598 0.006596 " +" 0.006133 0.958294 " +" 0.023598 0.006596 " +" 0.023598 0.993404 " +" 0.149209 0.001714 " +" 0.023598 0.993404 " +" 0.149209 0.001714 " +" 0.149209 0.998286 " +" 0.241706 0 " +" 0.149209 0.998286 " +" 0.241707 0 " +" 0.241706 1 " +" 0.758294 0 " +" 0.241707 1 " +" 0.758294 0 " +" 0.758294 1 " +" 0.850791 0.001714 " +" 0.758294 1 " +" 0.850791 0.001714 " +" 0.850791 0.998286 " +" 0.976402 0.006596 " +" 0.850791 0.998286 " +" 0.976402 0.006596 " +" 0.976402 0.993404 " +" 0.993867 0.041706 " +" 0.976402 0.993404 " +" 0.993867 0.041706 " +" 0.993867 0.958294 " +" 1 0.06756 " +" 0.993867 0.958294 " +" 1 0.06756 " +" 1 0.93244 " +" 1 0.93244 " +" 0.241706 1 " +" 0.241707 1 " +" 0.758294 1 " +" 0.149209 0.993867 " +" 0.758294 1 " +" 0.149209 0.993867 " +" 0.850791 0.993867 " +" 0.023598 0.976402 " +" 0.850791 0.993867 " +" 0.023598 0.976402 " +" 0.976402 0.976402 " +" 0.006133 0.850791 " +" 0.976402 0.976402 " +" 0.006133 0.850791 " +" 0.993867 0.850791 " +" 0 0.758293 " +" 0.993867 0.850791 " +" 0 0.758294 " +" 1 0.758294 " +" 0 0.241707 " +" 1 0.758294 " +" 0 0.241706 " +" 1 0.241707 " +" 0.006133 0.149209 " +" 1 0.241707 " +" 0.006133 0.149209 " +" 0.993867 0.149209 " +" 0.023598 0.023598 " +" 0.993867 0.149209 " +" 0.023598 0.023598 " +" 0.976402 0.023598 " +" 0.149209 0.006133 " +" 0.976402 0.023598 " +" 0.149209 0.006133 " +" 0.850791 0.006133 " +" 0.241707 0 " +" 0.850791 0.006133 " +" 0.241706 0 " +" 0.758294 0 " +" 0.758294 0 " +" }" +" }" +" }" +" }" +" }" +" }" +" osg::Geode {" +" UniqueID 38 " +" Name \"Cube\" " +" Drawables 1 {" +" osg::Geometry {" +" UniqueID 39 " +" DataVariance STATIC " +" StateSet TRUE {" +" osg::StateSet {" +" UniqueID 40 " +" DataVariance STATIC " +" AttributeList 1 {" +" osg::Material {" +" UniqueID 41 " +" Name \"Material\" " +" Ambient TRUE Front 1 1 1 1 Back 1 1 1 1 " +" Diffuse TRUE Front 0.8 0.8 0.8 1 Back 0.8 0.8 0.8 1 " +" Specular TRUE Front 0.5 0.5 0.5 1 Back 0.5 0.5 0.5 1 " +" Emission TRUE Front 0 0 0 1 Back 0 0 0 1 " +" Shininess TRUE Front 41.344 Back 41.344 " +" }" +" Value OFF " +" }" +" }" +" }" +" PrimitiveSetList 1 {" +" osg::DrawElementsUShort {" +" UniqueID 42 " +" BufferObject TRUE {" +" osg::ElementBufferObject {" +" UniqueID 43 " +" Target 34963 " +" }" +" }" +" Mode TRIANGLES " +" vector 36 {" +" 0 1 2 0 " +" 2 3 4 5 " +" 6 4 6 7 " +" 8 9 10 8 " +" 10 11 12 13 " +" 14 12 14 15 " +" 16 17 18 16 " +" 18 19 20 21 " +" 22 20 22 23 " +" " +" }" +" }" +" }" +" VertexArray TRUE {" +" osg::Vec3Array {" +" UniqueID 44 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 1 1 1 " +" -1 1 1 " +" -1 -1 1 " +" 1 -1 1 " +" 1 -1 -1 " +" 1 -1 1 " +" -1 -1 1 " +" -1 -1 -1 " +" -1 -1 -1 " +" -1 -1 1 " +" -1 1 1 " +" -1 1 -1 " +" -1 1 -1 " +" 1 1 -1 " +" 1 -1 -1 " +" -1 -1 -1 " +" 1 1 -1 " +" 1 1 1 " +" 1 -1 1 " +" 1 -1 -1 " +" -1 1 -1 " +" -1 1 1 " +" 1 1 1 " +" 1 1 -1 " +" }" +" }" +" }" +" NormalArray TRUE {" +" osg::Vec3Array {" +" UniqueID 46 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 0 0 1 " +" 0 0 1 " +" 0 0 1 " +" 0 0 1 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" 0 -1 0 " +" -1 0 0 " +" -1 0 0 " +" -1 0 0 " +" -1 0 0 " +" 0 0 -1 " +" 0 0 -1 " +" 0 0 -1 " +" 0 0 -1 " +" 1 0 0 " +" 1 0 0 " +" 1 0 0 " +" 1 0 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" 0 1 0 " +" }" +" }" +" }" +" TexCoordArrayList 1 {" +" osg::Vec2Array {" +" UniqueID 47 " +" BufferObject TRUE {" +" osg::VertexBufferObject {" +" UniqueID 45 " +" }" +" }" +" Binding BIND_PER_VERTEX " +" vector 24 {" +" 0.625 0.5 " +" 0.875 0.5 " +" 0.875 0.75 " +" 0.625 0.75 " +" 0.375 0.75 " +" 0.625 0.75 " +" 0.625 1 " +" 0.375 1 " +" 0.375 0 " +" 0.625 0 " +" 0.625 0.25 " +" 0.375 0.25 " +" 0.125 0.5 " +" 0.375 0.5 " +" 0.375 0.75 " +" 0.125 0.75 " +" 0.375 0.5 " +" 0.625 0.5 " +" 0.625 0.75 " +" 0.375 0.75 " +" 0.375 0.25 " +" 0.625 0.25 " +" 0.625 0.5 " +" 0.375 0.5 " +" }" +" }" +" }" +" }" +" }" +" }" +" }" +"}"; + +} diff --git a/components/misc/errorMarker.hpp b/components/misc/errorMarker.hpp new file mode 100644 index 0000000000..05e1fa9557 --- /dev/null +++ b/components/misc/errorMarker.hpp @@ -0,0 +1,11 @@ +#ifndef OPENMW_COMPONENTS_MISC_ERRORMARKER_H +#define OPENMW_COMPONENTS_MISC_ERRORMARKER_H + +#include + +namespace Misc +{ + extern const std::string errorMarker; +} + +#endif diff --git a/components/misc/hash.hpp b/components/misc/hash.hpp index 30a9c41ee9..861df73772 100644 --- a/components/misc/hash.hpp +++ b/components/misc/hash.hpp @@ -1,15 +1,20 @@ #ifndef MISC_HASH_H #define MISC_HASH_H +#include +#include +#include + namespace Misc { /// Implemented similar to the boost::hash_combine - template - inline void hashCombine(std::size_t& seed, const T& v) + template + inline void hashCombine(Seed& seed, const T& v) { + static_assert(sizeof(Seed) >= sizeof(std::size_t), "Resulting hash will be truncated"); std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + seed ^= static_cast(hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2)); } } -#endif \ No newline at end of file +#endif diff --git a/components/misc/osguservalues.cpp b/components/misc/osguservalues.cpp new file mode 100644 index 0000000000..3bdd0d1848 --- /dev/null +++ b/components/misc/osguservalues.cpp @@ -0,0 +1,6 @@ +#include "osguservalues.hpp" + +namespace Misc +{ + const std::string OsgUserValues::sFileHash = "fileHash"; +} diff --git a/components/misc/osguservalues.hpp b/components/misc/osguservalues.hpp new file mode 100644 index 0000000000..022e81764f --- /dev/null +++ b/components/misc/osguservalues.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H +#define OPENMW_COMPONENTS_MISC_OSGUSERVALUES_H + +#include + +namespace Misc +{ + struct OsgUserValues + { + static const std::string sFileHash; + }; +} + +#endif diff --git a/components/misc/progressreporter.hpp b/components/misc/progressreporter.hpp new file mode 100644 index 0000000000..733e36191e --- /dev/null +++ b/components/misc/progressreporter.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_MISC_PROGRESSREPORTER_H +#define OPENMW_COMPONENTS_MISC_PROGRESSREPORTER_H + +#include +#include +#include +#include +#include + +namespace Misc +{ + template + class ProgressReporter + { + public: + explicit ProgressReporter(Report&& report = Report {}) + : mReport(std::forward(report)) + {} + + explicit ProgressReporter(std::chrono::steady_clock::duration interval, Report&& report = Report {}) + : mInterval(interval) + , mReport(std::forward(report)) + {} + + void operator()(std::size_t provided, std::size_t expected) + { + expected = std::max(expected, provided); + const bool shouldReport = [&] + { + const std::lock_guard lock(mMutex); + const auto now = std::chrono::steady_clock::now(); + const auto left = mNextReport - now; + if (left.count() > 0 || provided == expected) + return false; + mNextReport += mInterval + left; + return true; + } (); + if (shouldReport) + mReport(provided, expected); + } + + private: + const std::chrono::steady_clock::duration mInterval = std::chrono::seconds(1); + Report mReport; + std::mutex mMutex; + std::chrono::steady_clock::time_point mNextReport {std::chrono::steady_clock::now() + mInterval}; + }; +} + +#endif diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 4d54baafc6..0095568653 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -30,17 +30,22 @@ namespace } -bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +bool changeExtension(std::string &path, std::string_view ext) { std::string::size_type pos = path.rfind('.'); - if(pos != std::string::npos && path.compare(pos, path.length() - pos, ".dds") != 0) + if(pos != std::string::npos && path.compare(pos, path.length() - pos, ext) != 0) { - path.replace(pos, path.length(), ".dds"); + path.replace(pos, path.length(), ext); return true; } return false; } +bool Misc::ResourceHelpers::changeExtensionToDds(std::string &path) +{ + return changeExtension(path, ".dds"); +} + std::string Misc::ResourceHelpers::correctResourcePath(const std::string &topLevelDirectory, const std::string &resPath, const VFS::Manager* vfs) { /* Bethesda at some point converted all their BSA @@ -140,7 +145,18 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP return mdlname; } +std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath, const VFS::Manager* vfs) +{ + std::string sound = resPath; + // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. + if (!vfs->exists(sound)) + changeExtension(sound, ".mp3"); + + return vfs->normalizeFilename(sound); + +} + bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id) { - return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; + return Misc::StringUtils::ciEqual(id, "prisonmarker") || Misc::StringUtils::ciEqual(id, "divinemarker") || Misc::StringUtils::ciEqual(id, "templemarker") || Misc::StringUtils::ciEqual(id, "northmarker"); } diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 9e87954e9d..4ea5f5e121 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -25,6 +25,8 @@ namespace Misc /// Use "xfoo.nif" instead of "foo.nif" if available std::string correctActorModelPath(const std::string &resPath, const VFS::Manager* vfs); + std::string correctSoundPath(const std::string& resPath, const VFS::Manager* vfs); + /// marker objects that have a hardcoded function in the game logic, should be hidden from the player bool isHiddenMarker(std::string_view id); } diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 0863522356..fe4be00006 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -6,8 +6,7 @@ #include #include #include - -#include "utf8stream.hpp" +#include namespace Misc { @@ -24,6 +23,7 @@ class StringUtils template static T argument(T value) noexcept { + static_assert(!std::is_same_v, "std::string_view is not supported"); return value; } @@ -43,70 +43,6 @@ public: return (c >= 'A' && c <= 'Z') ? c + 'a' - 'A' : c; } - static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch) - { - // Russian alphabet - if (ch >= 0x0410 && ch < 0x0430) - return ch + 0x20; - - // Cyrillic IO character - if (ch == 0x0401) - return ch + 0x50; - - // Latin alphabet - if (ch >= 0x41 && ch < 0x60) - return ch + 0x20; - - // Deutch characters - if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) - return ch + 0x20; - if (ch == 0x1e9e) - return 0xdf; - - // TODO: probably we will need to support characters from other languages - - return ch; - } - - static std::string lowerCaseUtf8(const std::string& str) - { - if (str.empty()) - return str; - - // Decode string as utf8 characters, convert to lower case and pack them to string - std::string out; - Utf8Stream stream (str.c_str()); - while (!stream.eof ()) - { - Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek()); - - if (character <= 0x7f) - out.append(1, static_cast(character)); - else if (character <= 0x7ff) - { - out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else if (character <= 0xffff) - { - out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - else - { - out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); - out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); - out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); - out.append(1, static_cast(0x80 | (character & 0x3f))); - } - - stream.consume(); - } - - return out; - } - static bool ciLess(const std::string &x, const std::string &y) { return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } @@ -182,63 +118,44 @@ public: return out; } - struct CiComp + struct CiEqual { bool operator()(const std::string& left, const std::string& right) const { - return ciLess(left, right); + return ciEqual(left, right); } }; - - - /// Performs a binary search on a sorted container for a string that 'key' starts with - template - static Iterator partialBinarySearch(Iterator begin, Iterator end, const T& key) + struct CiHash { - const Iterator notFound = end; - - while(begin < end) + std::size_t operator()(std::string str) const { - const Iterator middle = begin + (std::distance(begin, end) / 2); - - int comp = Misc::StringUtils::ciCompareLen((*middle), key, (*middle).size()); - - if(comp == 0) - return middle; - else if(comp > 0) - end = middle; - else - begin = middle + 1; + lowerCaseInPlace(str); + return std::hash{}(str); } - - return notFound; - } + }; + struct CiComp + { + bool operator()(const std::string& left, const std::string& right) const + { + return ciLess(left, right); + } + }; /** @brief Replaces all occurrences of a string in another string. * * @param str The string to operate on. * @param what The string to replace. * @param with The replacement string. - * @param whatLen The length of the string to replace. - * @param withLen The length of the replacement string. - * * @return A reference to the string passed in @p str. */ - static std::string &replaceAll(std::string &str, const char *what, const char *with, - std::size_t whatLen=std::string::npos, std::size_t withLen=std::string::npos) + static std::string &replaceAll(std::string &str, std::string_view what, std::string_view with) { - if (whatLen == std::string::npos) - whatLen = strlen(what); - - if (withLen == std::string::npos) - withLen = strlen(with); - std::size_t found; std::size_t offset = 0; - while((found = str.find(what, offset, whatLen)) != std::string::npos) + while((found = str.find(what, offset)) != std::string::npos) { - str.replace(found, whatLen, with, withLen); - offset = found + withLen; + str.replace(found, what.size(), with); + offset = found + with.size(); } return str; } @@ -267,17 +184,16 @@ public: static inline void trim(std::string &s) { - // left trim - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) + const auto notSpace = [](char ch) { - return !std::isspace(ch); - })); + // TODO Do we care about multibyte whitespace? + return !std::isspace(static_cast(ch)); + }; + // left trim + s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace)); // right trim - s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) - { - return !std::isspace(ch); - }).base(), s.end()); + s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end()); } template @@ -294,28 +210,19 @@ public: cont.push_back(str.substr(previous, current - previous)); } - // TODO: use the std::string_view once we will use the C++17. - // It should allow us to avoid data copying while we still will support both string and literal arguments. - - static inline void replaceAll(std::string& data, const std::string& toSearch, const std::string& replaceStr) + static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) { - size_t pos = data.find(toSearch); - - while( pos != std::string::npos) - { - data.replace(pos, toSearch.size(), replaceStr); - pos = data.find(toSearch, pos + replaceStr.size()); - } + size_t pos = str.rfind(substr); + if (pos == std::string::npos) + return; + str.replace(pos, substr.size(), with); } - static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with) - { - size_t pos = str.rfind(substr); - if (pos == std::string::npos) - return; - - str.replace(pos, substr.size(), with); - } + static inline bool ciEndsWith(std::string_view s, std::string_view suffix) + { + return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(), + [](char l, char r) { return toLower(l) == toLower(r); }); + }; }; } diff --git a/components/misc/utf8stream.hpp b/components/misc/utf8stream.hpp index e499d15e60..8435816c67 100644 --- a/components/misc/utf8stream.hpp +++ b/components/misc/utf8stream.hpp @@ -2,6 +2,8 @@ #define MISC_UTF8ITER_HPP #include +#include +#include #include class Utf8Stream @@ -29,6 +31,11 @@ public: { } + Utf8Stream (std::string_view str) : + Utf8Stream (reinterpret_cast(str.data()), reinterpret_cast(str.data() + str.size())) + { + } + bool eof () const { return cur == end; @@ -87,6 +94,70 @@ public: return std::make_pair (chr, cur); } + static UnicodeChar toLowerUtf8(UnicodeChar ch) + { + // Russian alphabet + if (ch >= 0x0410 && ch < 0x0430) + return ch + 0x20; + + // Cyrillic IO character + if (ch == 0x0401) + return ch + 0x50; + + // Latin alphabet + if (ch >= 0x41 && ch < 0x60) + return ch + 0x20; + + // German characters + if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc) + return ch + 0x20; + if (ch == 0x1e9e) + return 0xdf; + + // TODO: probably we will need to support characters from other languages + + return ch; + } + + static std::string lowerCaseUtf8(const std::string& str) + { + if (str.empty()) + return str; + + // Decode string as utf8 characters, convert to lower case and pack them to string + std::string out; + Utf8Stream stream (str.c_str()); + while (!stream.eof ()) + { + UnicodeChar character = toLowerUtf8(stream.peek()); + + if (character <= 0x7f) + out.append(1, static_cast(character)); + else if (character <= 0x7ff) + { + out.append(1, static_cast(0xc0 | ((character >> 6) & 0x1f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else if (character <= 0xffff) + { + out.append(1, static_cast(0xe0 | ((character >> 12) & 0x0f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + else + { + out.append(1, static_cast(0xf0 | ((character >> 18) & 0x07))); + out.append(1, static_cast(0x80 | ((character >> 12) & 0x3f))); + out.append(1, static_cast(0x80 | ((character >> 6) & 0x3f))); + out.append(1, static_cast(0x80 | (character & 0x3f))); + } + + stream.consume(); + } + + return out; + } + private: static std::pair octet_count (unsigned char octet) diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 3061b329c2..43b176c795 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -4,10 +4,8 @@ #include #include -#include #include #include -#include #include @@ -17,8 +15,6 @@ #include #include -#include - #include "myguicompat.h" #include "myguitexture.hpp" @@ -465,23 +461,17 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text batch.mVertexBuffer = static_cast(buffer)->getVertexBuffer(); batch.mArray = static_cast(buffer)->getVertexArray(); static_cast(buffer)->markUsed(); - bool premultipliedAlpha = false; - if (texture) + + if (OSGTexture* osgtexture = static_cast(texture)) { - batch.mTexture = static_cast(texture)->getTexture(); + batch.mTexture = osgtexture->getTexture(); if (batch.mTexture->getDataVariance() == osg::Object::DYNAMIC) mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin() - batch.mTexture->getUserValue("premultiplied alpha", premultipliedAlpha); + if (!mInjectState && osgtexture->getInjectState()) + batch.mStateSet = osgtexture->getInjectState(); } if (mInjectState) batch.mStateSet = mInjectState; - else if (premultipliedAlpha) - { - // This is hacky, but MyGUI made it impossible to use a custom layer for a nested node, so state couldn't be injected 'properly' - osg::ref_ptr stateSet = new osg::StateSet(); - stateSet->setAttribute(new osg::BlendFunc(osg::BlendFunc::ONE, osg::BlendFunc::ONE_MINUS_SRC_ALPHA)); - batch.mStateSet = stateSet; - } mDrawable->addBatch(batch); } diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index ce7332cc7e..d0e4e9a86e 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -21,9 +22,10 @@ namespace osgMyGUI { } - OSGTexture::OSGTexture(osg::Texture2D *texture) + OSGTexture::OSGTexture(osg::Texture2D *texture, osg::StateSet *injectState) : mImageManager(nullptr) , mTexture(texture) + , mInjectState(injectState) , mFormat(MyGUI::PixelFormat::Unknow) , mUsage(MyGUI::TextureUsage::Default) , mNumElemBytes(0) diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp index a34f1b7628..e8b49eab04 100644 --- a/components/myguiplatform/myguitexture.hpp +++ b/components/myguiplatform/myguitexture.hpp @@ -15,6 +15,7 @@ namespace osg { class Image; class Texture2D; + class StateSet; } namespace Resource @@ -31,6 +32,7 @@ namespace osgMyGUI osg::ref_ptr mLockedImage; osg::ref_ptr mTexture; + osg::ref_ptr mInjectState; MyGUI::PixelFormat mFormat; MyGUI::TextureUsage mUsage; size_t mNumElemBytes; @@ -40,9 +42,11 @@ namespace osgMyGUI public: OSGTexture(const std::string &name, Resource::ImageManager* imageManager); - OSGTexture(osg::Texture2D* texture); + OSGTexture(osg::Texture2D* texture, osg::StateSet* injectState = nullptr); virtual ~OSGTexture(); + osg::StateSet* getInjectState() { return mInjectState; } + const std::string& getName() const override { return mName; } void createManual(int width, int height, MyGUI::TextureUsage usage, MyGUI::PixelFormat format) override; diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp index 704c4928e6..16e6b5e40f 100644 --- a/components/nif/controller.cpp +++ b/components/nif/controller.cpp @@ -270,6 +270,15 @@ namespace Nif nif->getUInt(); // Zero } + void NiControllerManager::read(NIFStream *nif) + { + Controller::read(nif); + mCumulative = nif->getBoolean(); + unsigned int numSequences = nif->getUInt(); + nif->skip(4 * numSequences); // Controller sequences + nif->skip(4); // Object palette + } + void NiPoint3Interpolator::read(NIFStream *nif) { defaultVal = nif->getVector3(); diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index 503710fe90..210eaab217 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -184,6 +184,12 @@ struct bhkBlendController : public Controller void read(NIFStream *nif) override; }; +struct NiControllerManager : public Controller +{ + bool mCumulative; + void read(NIFStream *nif) override; +}; + struct Interpolator : public Record { }; struct NiPoint3Interpolator : public Interpolator diff --git a/components/nif/data.cpp b/components/nif/data.cpp index b6674611bc..cac924733d 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -34,6 +34,13 @@ void NiSkinInstance::post(NIFFile *nif) } } +void BSDismemberSkinInstance::read(NIFStream *nif) +{ + NiSkinInstance::read(nif); + unsigned int numPartitions = nif->getUInt(); + nif->skip(4 * numPartitions); // Body part information +} + void NiGeometryData::read(NIFStream *nif) { if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,114)) diff --git a/components/nif/data.hpp b/components/nif/data.hpp index efbe138c53..70171ac47c 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -170,6 +170,11 @@ struct NiSkinInstance : public Record void post(NIFFile *nif) override; }; +struct BSDismemberSkinInstance : public NiSkinInstance +{ + void read(NIFStream *nif) override; +}; + struct NiSkinData : public Record { struct VertWeight diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp index a45ea8c50b..04217995ac 100644 --- a/components/nif/extra.cpp +++ b/components/nif/extra.cpp @@ -94,4 +94,38 @@ void BSBound::read(NIFStream *nif) halfExtents = nif->getVector3(); } +void BSFurnitureMarker::LegacyFurniturePosition::read(NIFStream *nif) +{ + mOffset = nif->getVector3(); + mOrientation = nif->getUShort(); + mPositionRef = nif->getChar(); + nif->skip(1); // Position ref 2 +} + +void BSFurnitureMarker::FurniturePosition::read(NIFStream *nif) +{ + mOffset = nif->getVector3(); + mHeading = nif->getFloat(); + mType = nif->getUShort(); + mEntryPoint = nif->getUShort(); +} + +void BSFurnitureMarker::read(NIFStream *nif) +{ + Extra::read(nif); + unsigned int num = nif->getUInt(); + if (nif->getBethVersion() <= NIFFile::BethVersion::BETHVER_FO3) + { + mLegacyMarkers.resize(num); + for (auto& marker : mLegacyMarkers) + marker.read(nif); + } + else + { + mMarkers.resize(num); + for (auto& marker : mMarkers) + marker.read(nif); + } +} + } diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index f4ac1caff9..8eb14f9b01 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -120,5 +120,30 @@ struct BSBound : public Extra void read(NIFStream *nif) override; }; +struct BSFurnitureMarker : public Extra +{ + struct LegacyFurniturePosition + { + osg::Vec3f mOffset; + uint16_t mOrientation; + uint8_t mPositionRef; + void read(NIFStream *nif); + }; + + struct FurniturePosition + { + osg::Vec3f mOffset; + float mHeading; + uint16_t mType; + uint16_t mEntryPoint; + void read(NIFStream *nif); + }; + + std::vector mLegacyMarkers; + std::vector mMarkers; + + void read(NIFStream *nif) override; +}; + } // Namespace #endif diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 6863209988..1a1bbd7217 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -1,6 +1,8 @@ #include "niffile.hpp" #include "effect.hpp" +#include + #include #include #include @@ -136,6 +138,24 @@ static std::map makeFactory() factory["BSShaderProperty"] = {&construct , RC_BSShaderProperty }; factory["BSShaderPPLightingProperty"] = {&construct , RC_BSShaderPPLightingProperty }; factory["BSShaderNoLightingProperty"] = {&construct , RC_BSShaderNoLightingProperty }; + factory["BSFurnitureMarker"] = {&construct , RC_BSFurnitureMarker }; + factory["NiCollisionObject"] = {&construct , RC_NiCollisionObject }; + factory["bhkCollisionObject"] = {&construct , RC_bhkCollisionObject }; + factory["BSDismemberSkinInstance"] = {&construct , RC_BSDismemberSkinInstance }; + factory["NiControllerManager"] = {&construct , RC_NiControllerManager }; + factory["bhkMoppBvTreeShape"] = {&construct , RC_bhkMoppBvTreeShape }; + factory["bhkNiTriStripsShape"] = {&construct , RC_bhkNiTriStripsShape }; + factory["bhkPackedNiTriStripsShape"] = {&construct , RC_bhkPackedNiTriStripsShape }; + factory["hkPackedNiTriStripsData"] = {&construct , RC_hkPackedNiTriStripsData }; + factory["bhkConvexVerticesShape"] = {&construct , RC_bhkConvexVerticesShape }; + factory["bhkBoxShape"] = {&construct , RC_bhkBoxShape }; + factory["bhkListShape"] = {&construct , RC_bhkListShape }; + factory["bhkRigidBody"] = {&construct , RC_bhkRigidBody }; + factory["bhkRigidBodyT"] = {&construct , RC_bhkRigidBodyT }; + factory["BSLightingShaderProperty"] = {&construct , RC_BSLightingShaderProperty }; + factory["NiSortAdjustNode"] = {&construct , RC_NiNode }; + factory["NiClusterAccumulator"] = {&construct , RC_NiClusterAccumulator }; + factory["NiAlphaAccumulator"] = {&construct , RC_NiAlphaAccumulator }; return factory; } @@ -156,6 +176,9 @@ std::string NIFFile::printVersion(unsigned int version) void NIFFile::parse(Files::IStreamPtr stream) { + const std::array fileHash = Files::getHash(filename, *stream); + hash.append(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)); + NIFStream nif (this, stream); // Check the header string diff --git a/components/nif/niffile.hpp b/components/nif/niffile.hpp index 1ed7cbd5d8..6884f51d58 100644 --- a/components/nif/niffile.hpp +++ b/components/nif/niffile.hpp @@ -34,6 +34,8 @@ struct File virtual std::string getFilename() const = 0; + virtual std::string getHash() const = 0; + virtual unsigned int getVersion() const = 0; virtual unsigned int getUserVersion() const = 0; @@ -50,6 +52,7 @@ class NIFFile final : public File /// File name, used for error messages and opening the file std::string filename; + std::string hash; /// Record list std::vector records; @@ -141,6 +144,8 @@ public: /// Get the name of the file std::string getFilename() const override { return filename; } + std::string getHash() const override { return hash; } + /// Get the version of the NIF format used unsigned int getVersion() const override { return ver; } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index b6bf01ce58..04243c2506 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -149,6 +149,9 @@ public: inp->read(str.data(), length); if (inp->bad()) throw std::runtime_error("Failed to read sized string of " + std::to_string(length) + " chars"); + size_t end = str.find('\0'); + if (end != std::string::npos) + str.erase(end); return str; } ///Read in a string of the length specified in the file diff --git a/components/nif/node.hpp b/components/nif/node.hpp index 406a4d4549..7e5bcdb3be 100644 --- a/components/nif/node.hpp +++ b/components/nif/node.hpp @@ -8,6 +8,7 @@ #include "niftypes.hpp" #include "controller.hpp" #include "base.hpp" +#include "physics.hpp" #include @@ -143,6 +144,9 @@ struct Node : public Named bool hasBounds{false}; NiBoundingVolume bounds; + // Collision object info + NiCollisionObjectPtr collision; + void read(NIFStream *nif) override { Named::read(nif); @@ -160,7 +164,7 @@ struct Node : public Named bounds.read(nif); // Reference to the collision object in Gamebryo files. if (nif->getVersion() >= NIFStream::generateVersion(10,0,1,0)) - nif->skip(4); + collision.read(nif); parent = nullptr; @@ -171,6 +175,7 @@ struct Node : public Named { Named::post(nif); props.post(nif); + collision.post(nif); } // Parent node, or nullptr for the root node. As far as I'm aware, only @@ -422,5 +427,39 @@ struct NiLODNode : public NiSwitchNode } }; +// Abstract +struct NiAccumulator : Record +{ + void read(NIFStream *nif) override {} +}; + +// Node children sorters +struct NiClusterAccumulator : NiAccumulator {}; +struct NiAlphaAccumulator : NiClusterAccumulator {}; + +struct NiSortAdjustNode : NiNode +{ + enum SortingMode + { + SortingMode_Inherit, + SortingMode_Off, + SortingMode_Subsort + }; + + int mMode; + NiAccumulatorPtr mSubSorter; + void read(NIFStream *nif) override + { + NiNode::read(nif); + mMode = nif->getInt(); + if (nif->getVersion() <= NIFStream::generateVersion(20,0,0,3)) + mSubSorter.read(nif); + } + void post(NIFFile *nif) override + { + mSubSorter.post(nif); + } +}; + } // Namespace #endif diff --git a/components/nif/physics.cpp b/components/nif/physics.cpp new file mode 100644 index 0000000000..9bbeb148dd --- /dev/null +++ b/components/nif/physics.cpp @@ -0,0 +1,313 @@ +#include "physics.hpp" +#include "node.hpp" + +namespace Nif +{ + + /// Non-record data types + + void bhkWorldObjCInfoProperty::read(NIFStream *nif) + { + mData = nif->getUInt(); + mSize = nif->getUInt(); + mCapacityAndFlags = nif->getUInt(); + } + + void bhkWorldObjectCInfo::read(NIFStream *nif) + { + nif->skip(4); // Unused + mPhaseType = static_cast(nif->getChar()); + nif->skip(3); // Unused + mProperty.read(nif); + } + + void HavokMaterial::read(NIFStream *nif) + { + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD) + nif->skip(4); // Unknown + mMaterial = nif->getUInt(); + } + + void HavokFilter::read(NIFStream *nif) + { + mLayer = nif->getChar(); + mFlags = nif->getChar(); + mGroup = nif->getUShort(); + } + + void hkSubPartData::read(NIFStream *nif) + { + mHavokFilter.read(nif); + mNumVertices = nif->getUInt(); + mHavokMaterial.read(nif); + } + + void hkpMoppCode::read(NIFStream *nif) + { + unsigned int size = nif->getUInt(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + mOffset = nif->getVector4(); + if (nif->getBethVersion() > NIFFile::BethVersion::BETHVER_FO3) + nif->getChar(); // MOPP data build type + if (size) + nif->getChars(mData, size); + } + + void bhkEntityCInfo::read(NIFStream *nif) + { + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + + void TriangleData::read(NIFStream *nif) + { + for (int i = 0; i < 3; i++) + mTriangle[i] = nif->getUShort(); + mWeldingInfo = nif->getUShort(); + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB) + mNormal = nif->getVector3(); + } + + void bhkRigidBodyCInfo::read(NIFStream *nif) + { + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + { + nif->skip(4); // Unused + mHavokFilter.read(nif); + nif->skip(4); // Unused + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + { + if (nif->getBethVersion() >= 83) + nif->skip(4); // Unused + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + } + if (nif->getBethVersion() < 83) + nif->skip(4); // Unused + mTranslation = nif->getVector4(); + mRotation = nif->getQuaternion(); + mLinearVelocity = nif->getVector4(); + mAngularVelocity = nif->getVector4(); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 4; j++) + mInertiaTensor[i][j] = nif->getFloat(); + mCenter = nif->getVector4(); + mMass = nif->getFloat(); + mLinearDamping = nif->getFloat(); + mAngularDamping = nif->getFloat(); + if (nif->getBethVersion() >= 83) + { + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + mTimeFactor = nif->getFloat(); + mGravityFactor = nif->getFloat(); + } + mFriction = nif->getFloat(); + if (nif->getBethVersion() >= 83) + mRollingFrictionMult = nif->getFloat(); + mRestitution = nif->getFloat(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + { + mMaxLinearVelocity = nif->getFloat(); + mMaxAngularVelocity = nif->getFloat(); + if (nif->getBethVersion() != NIFFile::BethVersion::BETHVER_FO4) + mPenetrationDepth = nif->getFloat(); + } + mMotionType = static_cast(nif->getChar()); + if (nif->getBethVersion() < 83) + mDeactivatorType = static_cast(nif->getChar()); + else + mEnableDeactivation = nif->getBoolean(); + mSolverDeactivation = static_cast(nif->getChar()); + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4) + { + nif->skip(1); + mPenetrationDepth = nif->getFloat(); + mTimeFactor = nif->getFloat(); + nif->skip(4); + mResponseType = static_cast(nif->getChar()); + nif->skip(1); // Unused + mProcessContactDelay = nif->getUShort(); + } + mQualityType = static_cast(nif->getChar()); + if (nif->getBethVersion() >= 83) + { + mAutoRemoveLevel = nif->getChar(); + mResponseModifierFlags = nif->getChar(); + mNumContactPointShapeKeys = nif->getChar(); + mForceCollidedOntoPPU = nif->getBoolean(); + } + if (nif->getBethVersion() == NIFFile::BethVersion::BETHVER_FO4) + nif->skip(3); // Unused + else + nif->skip(12); // Unused + } + + /// Record types + + void bhkCollisionObject::read(NIFStream *nif) + { + NiCollisionObject::read(nif); + mFlags = nif->getUShort(); + mBody.read(nif); + } + + void bhkWorldObject::read(NIFStream *nif) + { + mShape.read(nif); + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB_OLD) + nif->skip(4); // Unknown + mHavokFilter.read(nif); + mWorldObjectInfo.read(nif); + } + + void bhkWorldObject::post(NIFFile *nif) + { + mShape.post(nif); + } + + void bhkEntity::read(NIFStream *nif) + { + bhkWorldObject::read(nif); + mInfo.read(nif); + } + + void bhkBvTreeShape::read(NIFStream *nif) + { + mShape.read(nif); + } + + void bhkBvTreeShape::post(NIFFile *nif) + { + mShape.post(nif); + } + + void bhkMoppBvTreeShape::read(NIFStream *nif) + { + bhkBvTreeShape::read(nif); + nif->skip(12); // Unused + mScale = nif->getFloat(); + mMopp.read(nif); + } + + void bhkNiTriStripsShape::read(NIFStream *nif) + { + mHavokMaterial.read(nif); + mRadius = nif->getFloat(); + nif->skip(20); // Unused + mGrowBy = nif->getUInt(); + if (nif->getVersion() >= NIFStream::generateVersion(10,1,0,0)) + mScale = nif->getVector4(); + mData.read(nif); + unsigned int numFilters = nif->getUInt(); + nif->getUInts(mFilters, numFilters); + } + + void bhkNiTriStripsShape::post(NIFFile *nif) + { + mData.post(nif); + } + + void bhkPackedNiTriStripsShape::read(NIFStream *nif) + { + if (nif->getVersion() <= NIFFile::NIFVersion::VER_OB) + { + mSubshapes.resize(nif->getUShort()); + for (hkSubPartData& subshape : mSubshapes) + subshape.read(nif); + } + mUserData = nif->getUInt(); + nif->skip(4); // Unused + mRadius = nif->getFloat(); + nif->skip(4); // Unused + mScale = nif->getVector4(); + nif->skip(20); // Duplicates of the two previous fields + mData.read(nif); + } + + void bhkPackedNiTriStripsShape::post(NIFFile *nif) + { + mData.post(nif); + } + + void hkPackedNiTriStripsData::read(NIFStream *nif) + { + unsigned int numTriangles = nif->getUInt(); + mTriangles.resize(numTriangles); + for (unsigned int i = 0; i < numTriangles; i++) + mTriangles[i].read(nif); + + unsigned int numVertices = nif->getUInt(); + bool compressed = false; + if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) + compressed = nif->getBoolean(); + if (!compressed) + nif->getVector3s(mVertices, numVertices); + else + nif->skip(6 * numVertices); // Half-precision vectors are not currently supported + if (nif->getVersion() >= NIFFile::NIFVersion::VER_BGS) + { + mSubshapes.resize(nif->getUShort()); + for (hkSubPartData& subshape : mSubshapes) + subshape.read(nif); + } + } + + void bhkSphereRepShape::read(NIFStream *nif) + { + mHavokMaterial.read(nif); + } + + void bhkConvexShape::read(NIFStream *nif) + { + bhkSphereRepShape::read(nif); + mRadius = nif->getFloat(); + } + + void bhkConvexVerticesShape::read(NIFStream *nif) + { + bhkConvexShape::read(nif); + mVerticesProperty.read(nif); + mNormalsProperty.read(nif); + unsigned int numVertices = nif->getUInt(); + if (numVertices) + nif->getVector4s(mVertices, numVertices); + unsigned int numNormals = nif->getUInt(); + if (numNormals) + nif->getVector4s(mNormals, numNormals); + } + + void bhkBoxShape::read(NIFStream *nif) + { + bhkConvexShape::read(nif); + nif->skip(8); // Unused + mExtents = nif->getVector3(); + nif->skip(4); // Unused + } + + void bhkListShape::read(NIFStream *nif) + { + mSubshapes.read(nif); + mHavokMaterial.read(nif); + mChildShapeProperty.read(nif); + mChildFilterProperty.read(nif); + unsigned int numFilters = nif->getUInt(); + mHavokFilters.resize(numFilters); + for (HavokFilter& filter : mHavokFilters) + filter.read(nif); + } + + void bhkRigidBody::read(NIFStream *nif) + { + bhkEntity::read(nif); + mInfo.read(nif); + mConstraints.read(nif); + if (nif->getBethVersion() < 76) + mBodyFlags = nif->getUInt(); + else + mBodyFlags = nif->getUShort(); + } + +} // Namespace \ No newline at end of file diff --git a/components/nif/physics.hpp b/components/nif/physics.hpp new file mode 100644 index 0000000000..613ec0ba43 --- /dev/null +++ b/components/nif/physics.hpp @@ -0,0 +1,332 @@ +#ifndef OPENMW_COMPONENTS_NIF_PHYSICS_HPP +#define OPENMW_COMPONENTS_NIF_PHYSICS_HPP + +#include "base.hpp" + +// This header contains certain record definitions +// specific to Bethesda implementation of Havok physics +namespace Nif +{ + +/// Non-record data types + +struct bhkWorldObjCInfoProperty +{ + unsigned int mData; + unsigned int mSize; + unsigned int mCapacityAndFlags; + void read(NIFStream *nif); +}; + +enum class BroadPhaseType : uint8_t +{ + BroadPhase_Invalid = 0, + BroadPhase_Entity = 1, + BroadPhase_Phantom = 2, + BroadPhase_Border = 3 +}; + +struct bhkWorldObjectCInfo +{ + BroadPhaseType mPhaseType; + bhkWorldObjCInfoProperty mProperty; + void read(NIFStream *nif); +}; + +struct HavokMaterial +{ + unsigned int mMaterial; + void read(NIFStream *nif); +}; + +struct HavokFilter +{ + unsigned char mLayer; + unsigned char mFlags; + unsigned short mGroup; + void read(NIFStream *nif); +}; + +struct hkSubPartData +{ + HavokMaterial mHavokMaterial; + unsigned int mNumVertices; + HavokFilter mHavokFilter; + void read(NIFStream *nif); +}; + +enum class hkResponseType : uint8_t +{ + Response_Invalid = 0, + Response_SimpleContact = 1, + Response_Reporting = 2, + Response_None = 3 +}; + +struct bhkEntityCInfo +{ + hkResponseType mResponseType; + unsigned short mProcessContactDelay; + void read(NIFStream *nif); +}; + +struct hkpMoppCode +{ + osg::Vec4f mOffset; + std::vector mData; + void read(NIFStream *nif); +}; + +struct TriangleData +{ + unsigned short mTriangle[3]; + unsigned short mWeldingInfo; + osg::Vec3f mNormal; + void read(NIFStream *nif); +}; + +enum class hkMotionType : uint8_t +{ + Motion_Invalid = 0, + Motion_Dynamic = 1, + Motion_SphereInertia = 2, + Motion_SphereStabilized = 3, + Motion_BoxInertia = 4, + Motion_BoxStabilized = 5, + Motion_Keyframed = 6, + Motion_Fixed = 7, + Motion_ThinBox = 8, + Motion_Character = 9 +}; + +enum class hkDeactivatorType : uint8_t +{ + Deactivator_Invalid = 0, + Deactivator_Never = 1, + Deactivator_Spatial = 2 +}; + +enum class hkSolverDeactivation : uint8_t +{ + SolverDeactivation_Invalid = 0, + SolverDeactivation_Off = 1, + SolverDeactivation_Low = 2, + SolverDeactivation_Medium = 3, + SolverDeactivation_High = 4, + SolverDeactivation_Max = 5 +}; + +enum class hkQualityType : uint8_t +{ + Quality_Invalid = 0, + Quality_Fixed = 1, + Quality_Keyframed = 2, + Quality_Debris = 3, + Quality_Moving = 4, + Quality_Critical = 5, + Quality_Bullet = 6, + Quality_User = 7, + Quality_Character = 8, + Quality_KeyframedReport = 9 +}; + +struct bhkRigidBodyCInfo +{ + HavokFilter mHavokFilter; + hkResponseType mResponseType; + unsigned short mProcessContactDelay; + osg::Vec4f mTranslation; + osg::Quat mRotation; + osg::Vec4f mLinearVelocity; + osg::Vec4f mAngularVelocity; + float mInertiaTensor[3][4]; + osg::Vec4f mCenter; + float mMass; + float mLinearDamping; + float mAngularDamping; + float mTimeFactor{1.f}; + float mGravityFactor{1.f}; + float mFriction; + float mRollingFrictionMult; + float mRestitution; + float mMaxLinearVelocity; + float mMaxAngularVelocity; + float mPenetrationDepth; + hkMotionType mMotionType; + hkDeactivatorType mDeactivatorType; + bool mEnableDeactivation{true}; + hkSolverDeactivation mSolverDeactivation; + hkQualityType mQualityType; + unsigned char mAutoRemoveLevel; + unsigned char mResponseModifierFlags; + unsigned char mNumContactPointShapeKeys; + bool mForceCollidedOntoPPU; + void read(NIFStream *nif); +}; + +/// Record types + +// Abstract Bethesda Havok object +struct bhkRefObject : public Record {}; + +// Abstract serializable Bethesda Havok object +struct bhkSerializable : public bhkRefObject {}; + +// Abstract narrowphase collision detection object +struct bhkShape : public bhkSerializable {}; + +// Abstract bhkShape collection +struct bhkShapeCollection : public bhkShape {}; + +// Generic collision object +struct NiCollisionObject : public Record +{ + // The node that references this object + NodePtr mTarget; + + void read(NIFStream *nif) override + { + mTarget.read(nif); + } + void post(NIFFile *nif) override + { + mTarget.post(nif); + } +}; + +// Bethesda Havok-specific collision object +struct bhkCollisionObject : public NiCollisionObject +{ + unsigned short mFlags; + bhkWorldObjectPtr mBody; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override + { + NiCollisionObject::post(nif); + mBody.post(nif); + } +}; + +// Abstract Havok shape info record +struct bhkWorldObject : public bhkSerializable +{ + bhkShapePtr mShape; + HavokFilter mHavokFilter; + bhkWorldObjectCInfo mWorldObjectInfo; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// Abstract +struct bhkEntity : public bhkWorldObject +{ + bhkEntityCInfo mInfo; + void read(NIFStream *nif) override; +}; + +// Bethesda extension of hkpBvTreeShape +// hkpBvTreeShape adds a bounding volume tree to an hkpShapeCollection +struct bhkBvTreeShape : public bhkShape +{ + bhkShapePtr mShape; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// bhkBvTreeShape with Havok MOPP code +struct bhkMoppBvTreeShape : public bhkBvTreeShape +{ + float mScale; + hkpMoppCode mMopp; + void read(NIFStream *nif) override; +}; + +// Bethesda triangle strip-based Havok shape collection +struct bhkNiTriStripsShape : public bhkShape +{ + HavokMaterial mHavokMaterial; + float mRadius; + unsigned int mGrowBy; + osg::Vec4f mScale{1.f, 1.f, 1.f, 0.f}; + NiTriStripsDataList mData; + std::vector mFilters; + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// Bethesda packed triangle strip-based Havok shape collection +struct bhkPackedNiTriStripsShape : public bhkShapeCollection +{ + std::vector mSubshapes; + unsigned int mUserData; + float mRadius; + osg::Vec4f mScale; + hkPackedNiTriStripsDataPtr mData; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + +// bhkPackedNiTriStripsShape data block +struct hkPackedNiTriStripsData : public bhkShapeCollection +{ + std::vector mTriangles; + std::vector mVertices; + std::vector mSubshapes; + void read(NIFStream *nif) override; +}; + +// Abstract +struct bhkSphereRepShape : public bhkShape +{ + HavokMaterial mHavokMaterial; + void read(NIFStream *nif) override; +}; + +// Abstract +struct bhkConvexShape : public bhkSphereRepShape +{ + float mRadius; + void read(NIFStream *nif) override; +}; + +// A convex shape built from vertices +struct bhkConvexVerticesShape : public bhkConvexShape +{ + bhkWorldObjCInfoProperty mVerticesProperty; + bhkWorldObjCInfoProperty mNormalsProperty; + std::vector mVertices; + std::vector mNormals; + void read(NIFStream *nif) override; +}; + +// A box +struct bhkBoxShape : public bhkConvexShape +{ + osg::Vec3f mExtents; + void read(NIFStream *nif) override; +}; + +// A list of shapes +struct bhkListShape : public bhkShapeCollection +{ + bhkShapeList mSubshapes; + HavokMaterial mHavokMaterial; + bhkWorldObjCInfoProperty mChildShapeProperty; + bhkWorldObjCInfoProperty mChildFilterProperty; + std::vector mHavokFilters; + void read(NIFStream *nif) override; +}; + +struct bhkRigidBody : public bhkEntity +{ + bhkRigidBodyCInfo mInfo; + bhkSerializableList mConstraints; + unsigned int mBodyFlags; + + void read(NIFStream *nif) override; +}; + +} // Namespace +#endif \ No newline at end of file diff --git a/components/nif/property.cpp b/components/nif/property.cpp index 1d2dd885d4..70acbb82ae 100644 --- a/components/nif/property.cpp +++ b/components/nif/property.cpp @@ -146,6 +146,62 @@ void BSShaderNoLightingProperty::read(NIFStream *nif) falloffParams = nif->getVector4(); } +void BSLightingShaderProperty::read(NIFStream *nif) +{ + type = nif->getUInt(); + BSShaderProperty::read(nif); + flags1 = nif->getUInt(); + flags2 = nif->getUInt(); + nif->skip(8); // UV offset + nif->skip(8); // UV scale + mTextureSet.read(nif); + mEmissive = nif->getVector3(); + mEmissiveMult = nif->getFloat(); + mClamp = nif->getUInt(); + mAlpha = nif->getFloat(); + nif->getFloat(); // Refraction strength + mGlossiness = nif->getFloat(); + mSpecular = nif->getVector3(); + mSpecStrength = nif->getFloat(); + nif->skip(8); // Lighting effects + switch (static_cast(type)) + { + case BSLightingShaderType::ShaderType_EnvMap: + nif->skip(4); // Environment map scale + break; + case BSLightingShaderType::ShaderType_SkinTint: + case BSLightingShaderType::ShaderType_HairTint: + nif->skip(12); // Tint color + break; + case BSLightingShaderType::ShaderType_ParallaxOcc: + nif->skip(4); // Max passes + nif->skip(4); // Scale + break; + case BSLightingShaderType::ShaderType_MultiLayerParallax: + nif->skip(4); // Inner layer thickness + nif->skip(4); // Refraction scale + nif->skip(4); // Inner layer texture scale + nif->skip(4); // Environment map strength + break; + case BSLightingShaderType::ShaderType_SparkleSnow: + nif->skip(16); // Sparkle parameters + break; + case BSLightingShaderType::ShaderType_EyeEnvmap: + nif->skip(4); // Cube map scale + nif->skip(12); // Left eye cube map offset + nif->skip(12); // Right eye cube map offset + break; + default: + break; + } +} + +void BSLightingShaderProperty::post(NIFFile *nif) +{ + BSShaderProperty::post(nif); + mTextureSet.post(nif); +} + void NiFogProperty::read(NIFStream *nif) { Property::read(nif); diff --git a/components/nif/property.hpp b/components/nif/property.hpp index 9c76f6d6ec..a428d6a7e1 100644 --- a/components/nif/property.hpp +++ b/components/nif/property.hpp @@ -116,20 +116,21 @@ struct NiShadeProperty : public Property } }; -struct BSShaderProperty : public NiShadeProperty + +enum class BSShaderType : unsigned int { - enum BSShaderType - { - SHADER_TALL_GRASS = 0, - SHADER_DEFAULT = 1, - SHADER_SKY = 10, - SHADER_SKIN = 14, - SHADER_WATER = 17, - SHADER_LIGHTING30 = 29, - SHADER_TILE = 32, - SHADER_NOLIGHTING = 33 - }; + ShaderType_TallGrass = 0, + ShaderType_Default = 1, + ShaderType_Sky = 10, + ShaderType_Skin = 14, + ShaderType_Water = 17, + ShaderType_Lighting30 = 29, + ShaderType_Tile = 32, + ShaderType_NoLighting = 33 +}; +struct BSShaderProperty : public NiShadeProperty +{ unsigned int type{0u}, flags1{0u}, flags2{0u}; float envMapIntensity{0.f}; void read(NIFStream *nif) override; @@ -169,6 +170,44 @@ struct BSShaderNoLightingProperty : public BSShaderLightingProperty void read(NIFStream *nif) override; }; +enum class BSLightingShaderType : unsigned int +{ + ShaderType_Default = 0, + ShaderType_EnvMap = 1, + ShaderType_Glow = 2, + ShaderType_Parallax = 3, + ShaderType_FaceTint = 4, + ShaderType_SkinTint = 5, + ShaderType_HairTint = 6, + ShaderType_ParallaxOcc = 7, + ShaderType_MultitexLand = 8, + ShaderType_LODLand = 9, + ShaderType_Snow = 10, + ShaderType_MultiLayerParallax = 11, + ShaderType_TreeAnim = 12, + ShaderType_LODObjects = 13, + ShaderType_SparkleSnow = 14, + ShaderType_LODObjectsHD = 15, + ShaderType_EyeEnvmap = 16, + ShaderType_Cloud = 17, + ShaderType_LODNoise = 18, + ShaderType_MultitexLandLODBlend = 19, + ShaderType_Dismemberment = 20 +}; + +struct BSLightingShaderProperty : public BSShaderProperty +{ + BSShaderTextureSetPtr mTextureSet; + unsigned int mClamp{0u}; + float mAlpha; + float mGlossiness; + osg::Vec3f mEmissive, mSpecular; + float mEmissiveMult, mSpecStrength; + + void read(NIFStream *nif) override; + void post(NIFFile *nif) override; +}; + struct NiDitherProperty : public Property { unsigned short flags; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index dc81eb69c7..937ce1d1e9 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -125,7 +125,24 @@ enum RecordType RC_BSLODTriShape, RC_BSShaderProperty, RC_BSShaderPPLightingProperty, - RC_BSShaderNoLightingProperty + RC_BSShaderNoLightingProperty, + RC_BSFurnitureMarker, + RC_NiCollisionObject, + RC_bhkCollisionObject, + RC_BSDismemberSkinInstance, + RC_NiControllerManager, + RC_bhkMoppBvTreeShape, + RC_bhkNiTriStripsShape, + RC_bhkPackedNiTriStripsShape, + RC_hkPackedNiTriStripsData, + RC_bhkConvexVerticesShape, + RC_bhkBoxShape, + RC_bhkListShape, + RC_bhkRigidBody, + RC_bhkRigidBodyT, + RC_BSLightingShaderProperty, + RC_NiClusterAccumulator, + RC_NiAlphaAccumulator }; /// Base class for all records diff --git a/components/nif/recordptr.hpp b/components/nif/recordptr.hpp index b30d99fbe4..5ec00b0c92 100644 --- a/components/nif/recordptr.hpp +++ b/components/nif/recordptr.hpp @@ -148,6 +148,12 @@ struct BSShaderTextureSet; struct NiGeometryData; struct BSShaderProperty; struct NiAlphaProperty; +struct NiCollisionObject; +struct bhkWorldObject; +struct bhkShape; +struct bhkSerializable; +struct hkPackedNiTriStripsData; +struct NiAccumulator; using NodePtr = RecordPtrT; using ExtraPtr = RecordPtrT; @@ -175,6 +181,11 @@ using BSShaderTextureSetPtr = RecordPtrT; using NiGeometryDataPtr = RecordPtrT; using BSShaderPropertyPtr = RecordPtrT; using NiAlphaPropertyPtr = RecordPtrT; +using NiCollisionObjectPtr = RecordPtrT; +using bhkWorldObjectPtr = RecordPtrT; +using bhkShapePtr = RecordPtrT; +using hkPackedNiTriStripsDataPtr = RecordPtrT; +using NiAccumulatorPtr = RecordPtrT; using NodeList = RecordListT; using PropertyList = RecordListT; @@ -182,6 +193,8 @@ using ExtraList = RecordListT; using NiSourceTextureList = RecordListT; using NiFloatInterpolatorList = RecordListT; using NiTriStripsDataList = RecordListT; +using bhkShapeList = RecordListT; +using bhkSerializableList = RecordListT; } // Namespace #endif diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 6ae1759395..4be07525a6 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -1,6 +1,8 @@ #include "bulletnifloader.hpp" +#include #include +#include #include #include @@ -18,11 +20,11 @@ namespace { -osg::Matrixf getWorldTransform(const Nif::Node *node) +osg::Matrixf getWorldTransform(const Nif::Node& node) { - if(node->parent != nullptr) - return node->trafo.toMatrix() * getWorldTransform(node->parent); - return node->trafo.toMatrix(); + if(node.parent != nullptr) + return node.trafo.toMatrix() * getWorldTransform(*node.parent); + return node.trafo.toMatrix(); } bool pathFileNameStartsWithX(const std::string& path) @@ -99,12 +101,56 @@ void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiTriStripsData& data, co } } -void fillTriangleMesh(btTriangleMesh& mesh, const Nif::NiGeometry* geometry, const osg::Matrixf &transform = osg::Matrixf()) +template +auto handleNiGeometry(const Nif::NiGeometry& geometry, Function&& function) + -> decltype(function(static_cast(geometry.data.get()))) { - if (geometry->recType == Nif::RC_NiTriShape || geometry->recType == Nif::RC_BSLODTriShape) - fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); - else if (geometry->recType == Nif::RC_NiTriStrips) - fillTriangleMesh(mesh, static_cast(geometry->data.get()), transform); + if (geometry.recType == Nif::RC_NiTriShape || geometry.recType == Nif::RC_BSLODTriShape) + { + if (geometry.data->recType != Nif::RC_NiTriShapeData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->triangles.empty()) + return {}; + + return function(static_cast(*data)); + } + + if (geometry.recType == Nif::RC_NiTriStrips) + { + if (geometry.data->recType != Nif::RC_NiTriStripsData) + return {}; + + auto data = static_cast(geometry.data.getPtr()); + if (data->strips.empty()) + return {}; + + return function(static_cast(*data)); + } + + return {}; +} + +std::monostate fillTriangleMesh(std::unique_ptr& mesh, const Nif::NiGeometry& geometry, const osg::Matrixf &transform) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + if (mesh == nullptr) + mesh.reset(new btTriangleMesh(false)); + fillTriangleMesh(*mesh, data, transform); + return std::monostate {}; + }); +} + +std::unique_ptr makeChildMesh(const Nif::NiGeometry& geometry) +{ + return handleNiGeometry(geometry, [&] (const auto& data) + { + std::unique_ptr mesh(new btTriangleMesh); + fillTriangleMesh(*mesh, data, osg::Matrixf()); + return mesh; + }); } } @@ -120,6 +166,8 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) mStaticMesh.reset(); mAvoidStaticMesh.reset(); + mShape->mFileHash = nif.getHash(); + const size_t numRoots = nif.numRoots(); std::vector roots; for (size_t i = 0; i < numRoots; ++i) @@ -132,6 +180,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) roots.emplace_back(node); } const std::string filename = nif.getFilename(); + mShape->mFileName = filename; if (roots.empty()) { warn("Found no root nodes in NIF file " + filename); @@ -141,10 +190,10 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Try to find a valid bounding box first. If one's found for any root node, use that. for (const Nif::Node* node : roots) { - if (findBoundingBox(node, filename)) + if (findBoundingBox(*node, filename)) { - const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.extents); - const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.center); + const btVector3 extents = Misc::Convert::toBullet(mShape->mCollisionBox.mExtents); + const btVector3 center = Misc::Convert::toBullet(mShape->mCollisionBox.mCenter); std::unique_ptr compound (new btCompoundShape); std::unique_ptr boxShape(new btBoxShape(extents)); btTransform transform = btTransform::getIdentity(); @@ -152,7 +201,7 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) compound->addChildShape(transform, boxShape.get()); boxShape.release(); - mShape->mCollisionShape = compound.release(); + mShape->mCollisionShape.reset(compound.release()); return mShape; } } @@ -164,13 +213,13 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // from the collision data present in every root node. for (const Nif::Node* node : roots) { - bool autogenerated = hasAutoGeneratedCollision(node); - handleNode(filename, node, 0, autogenerated, isAnimated, autogenerated); + bool autogenerated = hasAutoGeneratedCollision(*node); + handleNode(filename, *node, 0, autogenerated, isAnimated, autogenerated); } if (mCompoundShape) { - if (mStaticMesh) + if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { btTransform trans; trans.setIdentity(); @@ -179,17 +228,17 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) child.release(); mStaticMesh.release(); } - mShape->mCollisionShape = mCompoundShape.release(); + mShape->mCollisionShape = std::move(mCompoundShape); } - else if (mStaticMesh) + else if (mStaticMesh != nullptr && mStaticMesh->getNumTriangles() > 0) { - mShape->mCollisionShape = new Resource::TriangleMeshShape(mStaticMesh.get(), true); + mShape->mCollisionShape.reset(new Resource::TriangleMeshShape(mStaticMesh.get(), true)); mStaticMesh.release(); } - if (mAvoidStaticMesh) + if (mAvoidStaticMesh != nullptr && mAvoidStaticMesh->getNumTriangles() > 0) { - mShape->mAvoidCollisionShape = new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false); + mShape->mAvoidCollisionShape.reset(new Resource::TriangleMeshShape(mAvoidStaticMesh.get(), false)); mAvoidStaticMesh.release(); } @@ -198,41 +247,40 @@ osg::ref_ptr BulletNifLoader::load(const Nif::File& nif) // Find a boundingBox in the node hierarchy. // Return: use bounding box for collision? -bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& filename) +bool BulletNifLoader::findBoundingBox(const Nif::Node& node, const std::string& filename) { - if (node->hasBounds) + if (node.hasBounds) { - unsigned int type = node->bounds.type; + unsigned int type = node.bounds.type; switch (type) { case Nif::NiBoundingVolume::Type::BOX_BV: - mShape->mCollisionBox.extents = node->bounds.box.extents; - mShape->mCollisionBox.center = node->bounds.box.center; + mShape->mCollisionBox.mExtents = node.bounds.box.extents; + mShape->mCollisionBox.mCenter = node.bounds.box.center; break; default: { std::stringstream warning; - warning << "Unsupported NiBoundingVolume type " << type << " in node " << node->recIndex; + warning << "Unsupported NiBoundingVolume type " << type << " in node " << node.recIndex; warning << " in file " << filename; warn(warning.str()); } } - if (node->flags & Nif::NiNode::Flag_BBoxCollision) + if (node.flags & Nif::NiNode::Flag_BBoxCollision) { return true; } } - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { if(!list[i].empty()) { - if (findBoundingBox(list[i].getPtr(), filename)) + if (findBoundingBox(list[i].get(), filename)) return true; } } @@ -240,10 +288,9 @@ bool BulletNifLoader::findBoundingBox(const Nif::Node* node, const std::string& return false; } -bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) +bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node& rootNode) { - const Nif::NiNode *ninode = dynamic_cast(rootNode); - if(ninode) + if (const Nif::NiNode* ninode = dynamic_cast(&rootNode)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) @@ -258,32 +305,32 @@ bool BulletNifLoader::hasAutoGeneratedCollision(const Nif::Node* rootNode) return true; } -void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *node, int flags, +void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated, bool autogenerated, bool avoid) { // TODO: allow on-the fly collision switching via toggling this flag - if (node->recType == Nif::RC_NiCollisionSwitch && !(node->flags & Nif::NiNode::Flag_ActiveCollision)) + if (node.recType == Nif::RC_NiCollisionSwitch && !(node.flags & Nif::NiNode::Flag_ActiveCollision)) return; // Accumulate the flags from all the child nodes. This works for all // the flags we currently use, at least. - flags |= node->flags; + flags |= node.flags; - if (!node->controller.empty() && node->controller->recType == Nif::RC_NiKeyframeController - && (node->controller->flags & Nif::NiNode::ControllerFlag_Active)) + if (!node.controller.empty() && node.controller->recType == Nif::RC_NiKeyframeController + && (node.controller->flags & Nif::NiNode::ControllerFlag_Active)) isAnimated = true; - isCollisionNode = isCollisionNode || (node->recType == Nif::RC_RootCollisionNode); + isCollisionNode = isCollisionNode || (node.recType == Nif::RC_RootCollisionNode); // Don't collide with AvoidNode shapes - avoid = avoid || (node->recType == Nif::RC_AvoidNode); + avoid = avoid || (node.recType == Nif::RC_AvoidNode); // We encountered a RootCollisionNode inside autogenerated mesh. It is not right. - if (node->recType == Nif::RC_RootCollisionNode && autogenerated) + if (node.recType == Nif::RC_RootCollisionNode && autogenerated) Log(Debug::Info) << "RootCollisionNode is not attached to the root node in " << fileName << ". Treating it as a common NiTriShape."; // Check for extra data - for (Nif::ExtraPtr e = node->extra; !e.empty(); e = e->next) + for (Nif::ExtraPtr e = node.extra; !e.empty(); e = e->next) { if (e->recType == Nif::RC_NiStringExtraData) { @@ -310,108 +357,79 @@ void BulletNifLoader::handleNode(const std::string& fileName, const Nif::Node *n // NOTE: a trishape with hasBounds=true, but no BBoxCollision flag should NOT go through handleNiTriShape! // It must be ignored completely. // (occurs in tr_ex_imp_wall_arch_04.nif) - if(!node->hasBounds && (node->recType == Nif::RC_NiTriShape - || node->recType == Nif::RC_NiTriStrips - || node->recType == Nif::RC_BSLODTriShape)) + if(!node.hasBounds && (node.recType == Nif::RC_NiTriShape + || node.recType == Nif::RC_NiTriStrips + || node.recType == Nif::RC_BSLODTriShape)) { handleNiTriShape(node, flags, getWorldTransform(node), isAnimated, avoid); } } // For NiNodes, loop through children - const Nif::NiNode *ninode = dynamic_cast(node); - if(ninode) + if (const Nif::NiNode *ninode = dynamic_cast(&node)) { const Nif::NodeList &list = ninode->children; for(size_t i = 0;i < list.length();i++) { - if(!list[i].empty()) - handleNode(fileName, list[i].getPtr(), flags, isCollisionNode, isAnimated, autogenerated, avoid); + if (list[i].empty()) + continue; + + assert(list[i].get().parent == &node); + handleNode(fileName, list[i].get(), flags, isCollisionNode, isAnimated, autogenerated, avoid); } } } -void BulletNifLoader::handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf &transform, +void BulletNifLoader::handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf &transform, bool isAnimated, bool avoid) { - assert(nifNode != nullptr); - // If the object was marked "NCO" earlier, it shouldn't collide with // anything. So don't do anything. if ((flags & 0x800)) return; - auto niGeometry = static_cast(nifNode); - if (niGeometry->data.empty() || niGeometry->data->vertices.empty()) - return; - - if (niGeometry->recType == Nif::RC_NiTriShape || niGeometry->recType == Nif::RC_BSLODTriShape) - { - if (niGeometry->data->recType != Nif::RC_NiTriShapeData) - return; - - auto data = static_cast(niGeometry->data.getPtr()); - if (data->triangles.empty()) - return; - } - else if (niGeometry->recType == Nif::RC_NiTriStrips) - { - if (niGeometry->data->recType != Nif::RC_NiTriStripsData) - return; + handleNiTriShape(static_cast(nifNode), transform, isAnimated, avoid); +} - auto data = static_cast(niGeometry->data.getPtr()); - if (data->strips.empty()) - return; - } +void BulletNifLoader::handleNiTriShape(const Nif::NiGeometry& niGeometry, const osg::Matrixf &transform, + bool isAnimated, bool avoid) +{ + if (niGeometry.data.empty() || niGeometry.data->vertices.empty()) + return; - if (!niGeometry->skin.empty()) + if (!niGeometry.skin.empty()) isAnimated = false; if (isAnimated) { + std::unique_ptr childMesh = makeChildMesh(niGeometry); + if (childMesh == nullptr || childMesh->getNumTriangles() == 0) + return; + if (!mCompoundShape) mCompoundShape.reset(new btCompoundShape); - std::unique_ptr childMesh(new btTriangleMesh); - - fillTriangleMesh(*childMesh, niGeometry); - std::unique_ptr childShape(new Resource::TriangleMeshShape(childMesh.get(), true)); childMesh.release(); - float scale = nifNode->trafo.scale; - const Nif::Node* parent = nifNode; - while (parent->parent) - { - parent = parent->parent; + float scale = niGeometry.trafo.scale; + for (const Nif::Node* parent = niGeometry.parent; parent != nullptr; parent = parent->parent) scale *= parent->trafo.scale; - } osg::Quat q = transform.getRotate(); osg::Vec3f v = transform.getTrans(); childShape->setLocalScaling(btVector3(scale, scale, scale)); btTransform trans(btQuaternion(q.x(), q.y(), q.z(), q.w()), btVector3(v.x(), v.y(), v.z())); - mShape->mAnimatedShapes.emplace(nifNode->recIndex, mCompoundShape->getNumChildShapes()); + mShape->mAnimatedShapes.emplace(niGeometry.recIndex, mCompoundShape->getNumChildShapes()); mCompoundShape->addChildShape(trans, childShape.get()); childShape.release(); } else if (avoid) - { - if (!mAvoidStaticMesh) - mAvoidStaticMesh.reset(new btTriangleMesh(false)); - - fillTriangleMesh(*mAvoidStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mAvoidStaticMesh, niGeometry, transform); else - { - if (!mStaticMesh) - mStaticMesh.reset(new btTriangleMesh(false)); - - // Static shape, just transform all vertices into position - fillTriangleMesh(*mStaticMesh, niGeometry, transform); - } + fillTriangleMesh(mStaticMesh, niGeometry, transform); } } // namespace NifBullet diff --git a/components/nifbullet/bulletnifloader.hpp b/components/nifbullet/bulletnifloader.hpp index 71c84566a0..e0fec338c6 100644 --- a/components/nifbullet/bulletnifloader.hpp +++ b/components/nifbullet/bulletnifloader.hpp @@ -27,6 +27,7 @@ namespace Nif struct Transformation; struct NiTriShape; struct NiTriStrips; + struct NiGeometry; } namespace NifBullet @@ -52,16 +53,18 @@ public: osg::ref_ptr load(const Nif::File& file); private: - bool findBoundingBox(const Nif::Node* node, const std::string& filename); + bool findBoundingBox(const Nif::Node& node, const std::string& filename); - void handleNode(const std::string& fileName, Nif::Node const *node, int flags, bool isCollisionNode, + void handleNode(const std::string& fileName, const Nif::Node& node, int flags, bool isCollisionNode, bool isAnimated=false, bool autogenerated=false, bool avoid=false); - bool hasAutoGeneratedCollision(const Nif::Node *rootNode); + bool hasAutoGeneratedCollision(const Nif::Node& rootNode); - void handleNiTriShape(const Nif::Node *nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); + void handleNiTriShape(const Nif::Node& nifNode, int flags, const osg::Matrixf& transform, bool isAnimated, bool avoid); - std::unique_ptr mCompoundShape; + void handleNiTriShape(const Nif::NiGeometry& nifNode, const osg::Matrixf& transform, bool isAnimated, bool avoid); + + std::unique_ptr mCompoundShape; std::unique_ptr mStaticMesh; diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index ddded82156..54300d34e8 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -57,7 +57,7 @@ float ControllerFunction::calculate(float value) const } case Constant: default: - return std::min(mStopTime, std::max(mStartTime, time)); + return std::clamp(time, mStartTime, mStopTime); } } @@ -224,7 +224,7 @@ void GeomMorpherController::operator()(SceneUtil::MorphGeometry* node, osg::Node if (mKeyFrames.size() <= 1) return; float input = getInputValue(nv); - int i = 0; + int i = 1; for (std::vector::iterator it = mKeyFrames.begin()+1; it != mKeyFrames.end(); ++it,++i) { float val = 0; diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index c6311fd5fc..5d88dda1f1 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -248,6 +248,7 @@ namespace NifOsg META_Object(NifOsg, KeyframeController) osg::Vec3f getTranslation(float time) const override; + osg::Callback* getAsCallback() override { return this; } void operator() (NifOsg::MatrixTransform*, osg::NodeVisitor*); diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 838895eb47..99aaaa3323 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include // particle #include @@ -325,6 +325,8 @@ namespace NifOsg if (!textkeys->mTextKeys.empty()) created->getOrCreateUserDataContainer()->addUserObject(textkeys); + created->setUserValue(Misc::OsgUserValues::sFileHash, nif->getHash()); + return created; } @@ -1045,6 +1047,7 @@ namespace NifOsg void handleParticleSystem(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags) { osg::ref_ptr partsys (new ParticleSystem); + partsys->getOrCreateStateSet(); partsys->setSortMode(osgParticle::ParticleSystem::SORT_BACK_TO_FRONT); const Nif::NiParticleSystemController* partctrl = nullptr; @@ -1258,8 +1261,9 @@ namespace NifOsg const std::vector& morphs = morpher->data.getPtr()->mMorphs; if (morphs.empty()) return morphGeom; - // Note we are not interested in morph 0, which just contains the original vertices - for (unsigned int i = 1; i < morphs.size(); ++i) + if (morphs[0].mVertices.size() != static_cast(sourceGeometry->getVertexArray())->size()) + return morphGeom; + for (unsigned int i = 0; i < morphs.size(); ++i) morphGeom->addMorphTarget(new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f); return morphGeom; @@ -1714,28 +1718,64 @@ namespace NifOsg } } - const std::string& getNVShaderPrefix(unsigned int type) const + const std::string& getBSShaderPrefix(unsigned int type) const + { + static const std::unordered_map mapping = + { + {Nif::BSShaderType::ShaderType_TallGrass, std::string()}, + {Nif::BSShaderType::ShaderType_Default, "nv_default"}, + {Nif::BSShaderType::ShaderType_Sky, std::string()}, + {Nif::BSShaderType::ShaderType_Skin, std::string()}, + {Nif::BSShaderType::ShaderType_Water, std::string()}, + {Nif::BSShaderType::ShaderType_Lighting30, std::string()}, + {Nif::BSShaderType::ShaderType_Tile, std::string()}, + {Nif::BSShaderType::ShaderType_NoLighting, "nv_nolighting"}, + }; + auto prefix = mapping.find(static_cast(type)); + if (prefix == mapping.end()) + Log(Debug::Warning) << "Unknown BSShaderType " << type << " in " << mFilename; + else if (prefix->second.empty()) + Log(Debug::Warning) << "Unhandled BSShaderType " << type << " in " << mFilename; + else + return prefix->second; + + return mapping.at(Nif::BSShaderType::ShaderType_Default); + } + + const std::string& getBSLightingShaderPrefix(unsigned int type) const { - static const std::map mapping = - { - {Nif::BSShaderProperty::SHADER_TALL_GRASS, std::string()}, - {Nif::BSShaderProperty::SHADER_DEFAULT, "nv_default"}, - {Nif::BSShaderProperty::SHADER_SKY, std::string()}, - {Nif::BSShaderProperty::SHADER_SKIN, std::string()}, - {Nif::BSShaderProperty::SHADER_WATER, std::string()}, - {Nif::BSShaderProperty::SHADER_LIGHTING30, std::string()}, - {Nif::BSShaderProperty::SHADER_TILE, std::string()}, - {Nif::BSShaderProperty::SHADER_NOLIGHTING, "nv_nolighting"}, + static const std::unordered_map mapping = + { + {Nif::BSLightingShaderType::ShaderType_Default, "nv_default"}, + {Nif::BSLightingShaderType::ShaderType_EnvMap, std::string()}, + {Nif::BSLightingShaderType::ShaderType_Glow, std::string()}, + {Nif::BSLightingShaderType::ShaderType_Parallax, std::string()}, + {Nif::BSLightingShaderType::ShaderType_FaceTint, std::string()}, + {Nif::BSLightingShaderType::ShaderType_HairTint, std::string()}, + {Nif::BSLightingShaderType::ShaderType_ParallaxOcc, std::string()}, + {Nif::BSLightingShaderType::ShaderType_MultitexLand, std::string()}, + {Nif::BSLightingShaderType::ShaderType_LODLand, std::string()}, + {Nif::BSLightingShaderType::ShaderType_Snow, std::string()}, + {Nif::BSLightingShaderType::ShaderType_MultiLayerParallax, std::string()}, + {Nif::BSLightingShaderType::ShaderType_TreeAnim, std::string()}, + {Nif::BSLightingShaderType::ShaderType_LODObjects, std::string()}, + {Nif::BSLightingShaderType::ShaderType_SparkleSnow, std::string()}, + {Nif::BSLightingShaderType::ShaderType_LODObjectsHD, std::string()}, + {Nif::BSLightingShaderType::ShaderType_EyeEnvmap, std::string()}, + {Nif::BSLightingShaderType::ShaderType_Cloud, std::string()}, + {Nif::BSLightingShaderType::ShaderType_LODNoise, std::string()}, + {Nif::BSLightingShaderType::ShaderType_MultitexLandLODBlend, std::string()}, + {Nif::BSLightingShaderType::ShaderType_Dismemberment, std::string()} }; - auto prefix = mapping.find(type); + auto prefix = mapping.find(static_cast(type)); if (prefix == mapping.end()) - Log(Debug::Warning) << "Unknown shader type " << type << " in " << mFilename; + Log(Debug::Warning) << "Unknown BSLightingShaderType " << type << " in " << mFilename; else if (prefix->second.empty()) - Log(Debug::Warning) << "Unhandled shader type " << type << " in " << mFilename; + Log(Debug::Warning) << "Unhandled BSLightingShaderType " << type << " in " << mFilename; else return prefix->second; - return mapping.at(Nif::BSShaderProperty::SHADER_DEFAULT); + return mapping.at(Nif::BSLightingShaderType::ShaderType_Default); } void handleProperty(const Nif::Property *property, @@ -1795,7 +1835,7 @@ namespace NifOsg // Depth test flag stateset->setMode(GL_DEPTH_TEST, zprop->flags&1 ? osg::StateAttribute::ON : osg::StateAttribute::OFF); - auto depth = SceneUtil::createDepth(); + osg::ref_ptr depth = new osg::Depth; // Depth write flag depth->setWriteMask((zprop->flags>>1)&1); // Morrowind ignores depth test function @@ -1827,7 +1867,7 @@ namespace NifOsg { auto texprop = static_cast(property); bool shaderRequired = true; - node->setUserValue("shaderPrefix", getNVShaderPrefix(texprop->type)); + node->setUserValue("shaderPrefix", getBSShaderPrefix(texprop->type)); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); if (!texprop->textureSet.empty()) @@ -1842,7 +1882,7 @@ namespace NifOsg { auto texprop = static_cast(property); bool shaderRequired = true; - node->setUserValue("shaderPrefix", getNVShaderPrefix(texprop->type)); + node->setUserValue("shaderPrefix", getBSShaderPrefix(texprop->type)); node->setUserValue("shaderRequired", shaderRequired); osg::StateSet* stateset = node->getOrCreateStateSet(); if (!texprop->filename.empty()) @@ -1880,6 +1920,18 @@ namespace NifOsg handleTextureControllers(texprop, composite, imageManager, stateset, animflags); break; } + case Nif::RC_BSLightingShaderProperty: + { + auto texprop = static_cast(property); + bool shaderRequired = true; + node->setUserValue("shaderPrefix", getBSLightingShaderPrefix(texprop->type)); + node->setUserValue("shaderRequired", shaderRequired); + osg::StateSet* stateset = node->getOrCreateStateSet(); + if (!texprop->mTextureSet.empty()) + handleTextureSet(texprop->mTextureSet.getPtr(), texprop->mClamp, node->getName(), stateset, imageManager, boundTextures); + handleTextureControllers(texprop, composite, imageManager, stateset, animflags); + break; + } // unused by mw case Nif::RC_NiShadeProperty: case Nif::RC_NiDitherProperty: @@ -1931,6 +1983,7 @@ namespace NifOsg int lightmode = 1; float emissiveMult = 1.f; + float specStrength = 1.f; for (const Nif::Property* property : properties) { @@ -2027,6 +2080,17 @@ namespace NifOsg } break; } + case Nif::RC_BSLightingShaderProperty: + { + auto shaderprop = static_cast(property); + mat->setAlpha(osg::Material::FRONT_AND_BACK, shaderprop->mAlpha); + mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mEmissive, 1.f)); + mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(shaderprop->mSpecular, 1.f)); + mat->setShininess(osg::Material::FRONT_AND_BACK, shaderprop->mGlossiness); + emissiveMult = shaderprop->mEmissiveMult; + specStrength = shaderprop->mSpecStrength; + break; + } } } @@ -2080,6 +2144,8 @@ namespace NifOsg stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); if (emissiveMult != 1.f) stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); + if (specStrength != 1.f) + stateset->addUniform(new osg::Uniform("specStrength", specStrength)); } }; diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index 798a6778e6..74515691c6 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -10,86 +10,73 @@ namespace Resource { - -BulletShape::BulletShape() - : mCollisionShape(nullptr) - , mAvoidCollisionShape(nullptr) +namespace { + CollisionShapePtr duplicateCollisionShape(const btCollisionShape *shape) + { + if (shape == nullptr) + return nullptr; -} + if (shape->isCompound()) + { + const btCompoundShape *comp = static_cast(shape); + std::unique_ptr newShape(new btCompoundShape); -BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) - : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape)) - , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape)) - , mCollisionBox(copy.mCollisionBox) - , mAnimatedShapes(copy.mAnimatedShapes) -{ -} + for (int i = 0, n = comp->getNumChildShapes(); i < n; ++i) + { + auto child = duplicateCollisionShape(comp->getChildShape(i)); + const btTransform& trans = comp->getChildTransform(i); + newShape->addChildShape(trans, child.release()); + } -BulletShape::~BulletShape() -{ - deleteShape(mAvoidCollisionShape); - deleteShape(mCollisionShape); -} + return newShape; + } -void BulletShape::deleteShape(btCollisionShape* shape) -{ - if(shape!=nullptr) - { - if(shape->isCompound()) + if (shape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE) { - btCompoundShape* ms = static_cast(shape); - int a = ms->getNumChildShapes(); - for(int i=0; i getChildShape(i)); + const btBvhTriangleMeshShape* trishape = static_cast(shape); + return CollisionShapePtr(new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f))); } - delete shape; - } -} -btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *shape) const -{ - if(shape->isCompound()) - { - const btCompoundShape *comp = static_cast(shape); - btCompoundShape *newShape = new btCompoundShape; - - int numShapes = comp->getNumChildShapes(); - for(int i = 0;i < numShapes;++i) + if (shape->getShapeType() == BOX_SHAPE_PROXYTYPE) { - btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i)); - const btTransform& trans = comp->getChildTransform(i); - newShape->addChildShape(trans, child); + const btBoxShape* boxshape = static_cast(shape); + return CollisionShapePtr(new btBoxShape(*boxshape)); } - return newShape; - } + if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) + return CollisionShapePtr(new btHeightfieldTerrainShape(static_cast(*shape))); - if(const btBvhTriangleMeshShape* trishape = dynamic_cast(shape)) - { - btScaledBvhTriangleMeshShape* newShape = new btScaledBvhTriangleMeshShape(const_cast(trishape), btVector3(1.f, 1.f, 1.f)); - return newShape; + throw std::logic_error(std::string("Unhandled Bullet shape duplication: ") + shape->getName()); } - if (const btBoxShape* boxshape = dynamic_cast(shape)) + void deleteShape(btCollisionShape* shape) { - return new btBoxShape(*boxshape); - } - - if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) - return new btHeightfieldTerrainShape(static_cast(*shape)); + if (shape->isCompound()) + { + btCompoundShape* compound = static_cast(shape); + for (int i = 0, n = compound->getNumChildShapes(); i < n; i++) + if (btCollisionShape* child = compound->getChildShape(i)) + deleteShape(child); + } - throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName()); + delete shape; + } } -btCollisionShape *BulletShape::getCollisionShape() const +void DeleteCollisionShape::operator()(btCollisionShape* shape) const { - return mCollisionShape; + deleteShape(shape); } -btCollisionShape *BulletShape::getAvoidCollisionShape() const +BulletShape::BulletShape(const BulletShape ©, const osg::CopyOp ©op) + : mCollisionShape(duplicateCollisionShape(copy.mCollisionShape.get())) + , mAvoidCollisionShape(duplicateCollisionShape(copy.mAvoidCollisionShape.get())) + , mCollisionBox(copy.mCollisionBox) + , mAnimatedShapes(copy.mAnimatedShapes) + , mFileName(copy.mFileName) + , mFileHash(copy.mFileHash) { - return mAvoidCollisionShape; } void BulletShape::setLocalScaling(const btVector3& scale) @@ -99,30 +86,18 @@ void BulletShape::setLocalScaling(const btVector3& scale) mAvoidCollisionShape->setLocalScaling(scale); } -bool BulletShape::isAnimated() const +osg::ref_ptr makeInstance(osg::ref_ptr source) { - return !mAnimatedShapes.empty(); -} - -osg::ref_ptr BulletShape::makeInstance() const -{ - osg::ref_ptr instance (new BulletShapeInstance(this)); - return instance; + return {new BulletShapeInstance(std::move(source))}; } BulletShapeInstance::BulletShapeInstance(osg::ref_ptr source) - : BulletShape() - , mSource(source) + : mSource(std::move(source)) { - mCollisionBox = source->mCollisionBox; - - mAnimatedShapes = source->mAnimatedShapes; - - if (source->mCollisionShape) - mCollisionShape = duplicateCollisionShape(source->mCollisionShape); - - if (source->mAvoidCollisionShape) - mAvoidCollisionShape = duplicateCollisionShape(source->mAvoidCollisionShape); + mCollisionBox = mSource->mCollisionBox; + mAnimatedShapes = mSource->mAnimatedShapes; + mCollisionShape = duplicateCollisionShape(mSource->mCollisionShape.get()); + mAvoidCollisionShape = duplicateCollisionShape(mSource->mAvoidCollisionShape.get()); } } diff --git a/components/resource/bulletshape.hpp b/components/resource/bulletshape.hpp index 6ac8064cb3..cd8922ec8e 100644 --- a/components/resource/bulletshape.hpp +++ b/components/resource/bulletshape.hpp @@ -1,7 +1,9 @@ #ifndef OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H #define OPENMW_COMPONENTS_RESOURCE_BULLETSHAPE_H +#include #include +#include #include #include @@ -11,26 +13,35 @@ class btCollisionShape; +namespace NifBullet +{ + class BulletNifLoader; +} + namespace Resource { + struct DeleteCollisionShape + { + void operator()(btCollisionShape* shape) const; + }; + + using CollisionShapePtr = std::unique_ptr; - class BulletShapeInstance; class BulletShape : public osg::Object { public: - BulletShape(); + BulletShape() = default; BulletShape(const BulletShape& copy, const osg::CopyOp& copyop); - virtual ~BulletShape(); META_Object(Resource, BulletShape) - btCollisionShape* mCollisionShape; - btCollisionShape* mAvoidCollisionShape; + CollisionShapePtr mCollisionShape; + CollisionShapePtr mAvoidCollisionShape; struct CollisionBox { - osg::Vec3f extents; - osg::Vec3f center; + osg::Vec3f mExtents; + osg::Vec3f mCenter; }; // Used for actors and projectiles. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures. // For now, use one file <-> one resource for simplicity. @@ -42,21 +53,12 @@ namespace Resource // we store the node's record index mapped to the child index of the shape in the btCompoundShape. std::map mAnimatedShapes; - osg::ref_ptr makeInstance() const; - - btCollisionShape* duplicateCollisionShape(const btCollisionShape* shape) const; - - btCollisionShape* getCollisionShape() const; - - btCollisionShape* getAvoidCollisionShape() const; + std::string mFileName; + std::string mFileHash; void setLocalScaling(const btVector3& scale); - bool isAnimated() const; - - private: - - void deleteShape(btCollisionShape* shape); + bool isAnimated() const { return !mAnimatedShapes.empty(); } }; @@ -67,10 +69,14 @@ namespace Resource public: BulletShapeInstance(osg::ref_ptr source); + const osg::ref_ptr& getSource() const { return mSource; } + private: osg::ref_ptr mSource; }; + osg::ref_ptr makeInstance(osg::ref_ptr source); + // Subclass btBhvTriangleMeshShape to auto-delete the meshInterface struct TriangleMeshShape : public btBvhTriangleMeshShape { diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index 0d0f81962b..da4672757a 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -1,16 +1,18 @@ #include "bulletshapemanager.hpp" +#include + #include #include #include #include -#include #include #include #include #include +#include #include @@ -45,11 +47,7 @@ struct GetTriangleFunctor return btVector3(vec.x(), vec.y(), vec.z()); } -#if OSG_MIN_VERSION_REQUIRED(3,5,6) - void inline operator()( const osg::Vec3& v1, const osg::Vec3& v2, const osg::Vec3& v3 ) -#else - void inline operator()( const osg::Vec3& v1, const osg::Vec3& v2, const osg::Vec3& v3, bool _temp ) -#endif + void inline operator()( const osg::Vec3& v1, const osg::Vec3& v2, const osg::Vec3& v3, bool _temp=false ) // Note: unused temp argument left here for OSG versions less than 3.5.6 { if (mTriMesh) mTriMesh->addTriangle( toBullet(mMatrix.preMult(v1)), toBullet(mMatrix.preMult(v2)), toBullet(mMatrix.preMult(v3))); @@ -88,16 +86,17 @@ public: return osg::ref_ptr(); osg::ref_ptr shape (new BulletShape); - btBvhTriangleMeshShape* triangleMeshShape = new TriangleMeshShape(mTriangleMesh.release(), true); + + auto triangleMeshShape = std::make_unique(mTriangleMesh.release(), true); btVector3 aabbMin = triangleMeshShape->getLocalAabbMin(); btVector3 aabbMax = triangleMeshShape->getLocalAabbMax(); - shape->mCollisionBox.extents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; - shape->mCollisionBox.extents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; - shape->mCollisionBox.extents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; - shape->mCollisionBox.center = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, + shape->mCollisionBox.mExtents[0] = (aabbMax[0] - aabbMin[0]) / 2.0f; + shape->mCollisionBox.mExtents[1] = (aabbMax[1] - aabbMin[1]) / 2.0f; + shape->mCollisionBox.mExtents[2] = (aabbMax[2] - aabbMin[2]) / 2.0f; + shape->mCollisionBox.mCenter = osg::Vec3f( (aabbMax[0] + aabbMin[0]) / 2.0f, (aabbMax[1] + aabbMin[1]) / 2.0f, (aabbMax[2] + aabbMin[2]) / 2.0f ); - shape->mCollisionShape = triangleMeshShape; + shape->mCollisionShape.reset(triangleMeshShape.release()); return shape; } @@ -166,6 +165,12 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & if (!shape) return osg::ref_ptr(); } + + if (shape != nullptr) + { + shape->mFileName = normalized; + constNode->getUserValue(Misc::OsgUserValues::sFileHash, shape->mFileHash); + } } mCache->addEntryToObjectCache(normalized, shape); @@ -198,9 +203,8 @@ osg::ref_ptr BulletShapeManager::createInstance(const std:: { osg::ref_ptr shape = getShape(name); if (shape) - return shape->makeInstance(); - else - return osg::ref_ptr(); + return makeInstance(std::move(shape)); + return osg::ref_ptr(); } void BulletShapeManager::updateCache(double referenceTime) diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index c5ef957c3e..5f2d78d2ed 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include @@ -29,10 +32,14 @@ #include #include #include +#include #include #include +#include +#include + #include "imagemanager.hpp" #include "niffilemanager.hpp" #include "objectcache.hpp" @@ -246,14 +253,14 @@ namespace Resource { if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN) { - osg::ref_ptr depth = SceneUtil::createDepth(); + osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); } else if (stateset->getRenderingHint() == osg::StateSet::OPAQUE_BIN) { - osg::ref_ptr depth = SceneUtil::createDepth(); + osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(true); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); @@ -429,6 +436,11 @@ namespace Resource mConvertAlphaTestToAlphaToCoverage = convert; } + void SceneManager::setOpaqueDepthTex(osg::ref_ptr texture) + { + mOpaqueDepthTex = texture; + } + SceneManager::~SceneManager() { // this has to be defined in the .cpp file as we can't delete incomplete types @@ -474,13 +486,11 @@ namespace Resource Resource::ImageManager* mImageManager; }; - osg::ref_ptr load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) + namespace { - auto ext = Misc::getFileExtension(normalizedFilename); - if (ext == "nif") - return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); - else + osg::ref_ptr loadNonNif(const std::string& normalizedFilename, std::istream& model, Resource::ImageManager* imageManager) { + auto ext = Misc::getFileExtension(normalizedFilename); osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext)); if (!reader) { @@ -496,7 +506,9 @@ namespace Resource options->setReadFileCallback(new ImageReadCallback(imageManager)); if (ext == "dae") options->setOptionString("daeUseSequencedTextureUnits"); - osgDB::ReaderWriter::ReadResult result = reader->readNode(*vfs->get(normalizedFilename), options); + const std::array fileHash = Files::getHash(normalizedFilename, model); + + osgDB::ReaderWriter::ReadResult result = reader->readNode(model, options); if (!result.success()) { std::stringstream errormsg; @@ -507,7 +519,9 @@ namespace Resource // Recognize and hide collision node unsigned int hiddenNodeMask = 0; SceneUtil::FindByNameVisitor nameFinder("Collision"); - result.getNode()->accept(nameFinder); + + auto node = result.getNode(); + node->accept(nameFinder); if (nameFinder.mFoundNode) nameFinder.mFoundNode->setNodeMask(hiddenNodeMask); @@ -515,18 +529,30 @@ namespace Resource { // Collada alpha testing Resource::ColladaAlphaTrickVisitor colladaAlphaTrickVisitor; - result.getNode()->accept(colladaAlphaTrickVisitor); + node->accept(colladaAlphaTrickVisitor); - result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); - result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); - result.getNode()->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("specStrength", 1.f)); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1))); + node->getOrCreateStateSet()->addUniform(new osg::Uniform("useFalloff", false)); } + node->setUserValue(Misc::OsgUserValues::sFileHash, + std::string(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t))); - return result.getNode(); + return node; } } + osg::ref_ptr load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) + { + auto ext = Misc::getFileExtension(normalizedFilename); + if (ext == "nif") + return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); + else + return loadNonNif(normalizedFilename, *vfs->get(normalizedFilename), imageManager); + } + class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback { public: @@ -551,7 +577,7 @@ namespace Resource std::sort(reservedNames.begin(), reservedNames.end(), Misc::StringUtils::ciLess); } - std::vector::iterator it = Misc::StringUtils::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); + std::vector::iterator it = Misc::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); return it != reservedNames.end(); } @@ -642,23 +668,23 @@ namespace Resource { loaded = load(normalized, mVFS, mImageManager, mNifFileManager); } - catch (std::exception& e) + catch (const std::exception& e) { - static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" }; + static osg::ref_ptr errorMarkerNode = [&] { + static const char* const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae" }; - for (unsigned int i=0; iexists(normalized)) + for (unsigned int i=0; iexists(normalized)) + return load(normalized, mVFS, mImageManager, mNifFileManager); } - } + Files::IMemStream file(Misc::errorMarker.data(), Misc::errorMarker.size()); + return loadNonNif("error_marker.osgt", file, mImageManager); + }(); - if (!loaded) - throw; + Log(Debug::Error) << "Failed to load '" << name << "': " << e.what() << ", using marker_error instead"; + loaded = static_cast(errorMarkerNode->clone(osg::CopyOp::DEEP_COPY_ALL)); } // set filtering settings @@ -667,6 +693,9 @@ namespace Resource SetFilterSettingsControllerVisitor setFilterSettingsControllerVisitor(mMinFilter, mMagFilter, mMaxAnisotropy); loaded->accept(setFilterSettingsControllerVisitor); + SceneUtil::ReplaceDepthVisitor replaceDepthVisitor; + loaded->accept(replaceDepthVisitor); + osg::ref_ptr shaderVisitor (createShaderVisitor()); loaded->accept(*shaderVisitor); @@ -693,19 +722,33 @@ namespace Resource } } - osg::ref_ptr SceneManager::createInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); - return createInstance(scene); + return getInstance(scene); } - osg::ref_ptr SceneManager::createInstance(const osg::Node *base) + osg::ref_ptr SceneManager::cloneNode(const osg::Node* base) { - osg::ref_ptr cloned = static_cast(base->clone(SceneUtil::CopyOp())); - - // add a ref to the original template, to hint to the cache that it's still being used and should be kept in cache + SceneUtil::CopyOp copyop; + if (const osg::Drawable* drawable = base->asDrawable()) + { + if (drawable->asGeometry()) + { + Log(Debug::Warning) << "SceneManager::cloneNode: attempting to clone osg::Geometry. For safety reasons this will be expensive. Consider avoiding this call."; + copyop.setCopyFlags(copyop.getCopyFlags()|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES); + } + } + osg::ref_ptr cloned = static_cast(base->clone(copyop)); + // add a ref to the original template to help verify the safety of shallow cloning operations + // in addition, if this node is managed by a cache, we hint to the cache that it's still being used and should be kept in cache cloned->getOrCreateUserDataContainer()->addUserObject(new TemplateRef(base)); + return cloned; + } + osg::ref_ptr SceneManager::getInstance(const osg::Node *base) + { + osg::ref_ptr cloned = cloneNode(base); // we can skip any scene graphs without update callbacks since we know that particle emitters will have an update callback set if (cloned->getNumChildrenRequiringUpdateTraversal() > 0) { @@ -716,11 +759,6 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string &name) - { - return createInstance(name); - } - osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); @@ -882,6 +920,7 @@ namespace Resource shaderVisitor->setSpecularMapPattern(mSpecularMapPattern); shaderVisitor->setApplyLightingToEnvMaps(mApplyLightingToEnvMaps); shaderVisitor->setConvertAlphaTestToAlphaToCoverage(mConvertAlphaTestToAlphaToCoverage); + shaderVisitor->setOpaqueDepthTex(mOpaqueDepthTex); return shaderVisitor; } } diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index b5d3e453a0..58ae8fdb8b 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "resourcemanager.hpp" @@ -111,6 +112,8 @@ namespace Resource void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + void setOpaqueDepthTex(osg::ref_ptr texture); + enum class UBOBinding { // If we add more UBO's, we should probably assign their bindings dynamically according to the current count of UBO's in the programTemplate @@ -132,16 +135,22 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - osg::ref_ptr createInstance(const std::string& name); + /// Clone osg::Node safely. + /// @note Thread safe. + static osg::ref_ptr cloneNode(const osg::Node* base); - osg::ref_ptr createInstance(const osg::Node* base); void shareState(osg::ref_ptr node); - /// Get an instance of the given scene template + + /// Clone osg::Node and adjust it according to SceneManager's settings. + /// @note Thread safe. + osg::ref_ptr getInstance(const osg::Node* base); + + /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. osg::ref_ptr getInstance(const std::string& name); - /// Get an instance of the given scene template and immediately attach it to a parent node + /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); @@ -203,6 +212,7 @@ namespace Resource SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; bool mConvertAlphaTestToAlphaToCoverage; GLenum mDepthFormat; + osg::ref_ptr mOpaqueDepthTex; osg::ref_ptr mSharedStateManager; mutable std::mutex mSharedStateMutex; diff --git a/components/sceneutil/agentpath.cpp b/components/sceneutil/agentpath.cpp index 5f9b574e7d..5721110f77 100644 --- a/components/sceneutil/agentpath.cpp +++ b/components/sceneutil/agentpath.cpp @@ -43,7 +43,7 @@ namespace SceneUtil const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1); + DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1); const auto agentRadius = halfExtents.x(); const auto agentHeight = 2.0f * halfExtents.z(); diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 6690148c74..02c3456425 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -13,9 +13,9 @@ #include #include +#include #include "visitor.hpp" -#include "clone.hpp" namespace SceneUtil { @@ -49,10 +49,10 @@ namespace SceneUtil if (!filterMatches(drawable.getName())) return; - osg::Node* node = &drawable; + const osg::Node* node = &drawable; for (auto it = getNodePath().rbegin()+1; it != getNodePath().rend(); ++it) { - osg::Node* parent = *it; + const osg::Node* parent = *it; if (!filterMatches(parent->getName())) break; node = parent; @@ -60,11 +60,11 @@ namespace SceneUtil mToCopy.emplace(node); } - void doCopy() + void doCopy(Resource::SceneManager* sceneManager) { - for (const osg::ref_ptr& node : mToCopy) + for (const osg::ref_ptr& node : mToCopy) { - mParent->addChild(static_cast(node->clone(SceneUtil::CopyOp()))); + mParent->addChild(sceneManager->getInstance(node)); } mToCopy.clear(); } @@ -78,7 +78,7 @@ namespace SceneUtil || (lowerName.size() >= mFilter2.size() && lowerName.compare(0, mFilter2.size(), mFilter2) == 0); } - using NodeSet = std::set>; + using NodeSet = std::set>; NodeSet mToCopy; osg::ref_ptr mParent; @@ -100,7 +100,7 @@ namespace SceneUtil } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode, Resource::SceneManager* sceneManager, const osg::Quat* attitude) { if (dynamic_cast(toAttach.get())) { @@ -108,7 +108,9 @@ namespace SceneUtil CopyRigVisitor copyVisitor(handle, filter); const_cast(toAttach.get())->accept(copyVisitor); - copyVisitor.doCopy(); + copyVisitor.doCopy(sceneManager); + // add a ref to the original template to hint to the cache that it is still being used and should be kept in cache. + handle->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(toAttach)); if (handle->getNumChildren() == 1) { @@ -127,7 +129,7 @@ namespace SceneUtil } else { - osg::ref_ptr clonedToAttach = static_cast(toAttach->clone(SceneUtil::CopyOp())); + osg::ref_ptr clonedToAttach = sceneManager->getInstance(toAttach); FindByNameVisitor findBoneOffset("BoneOffset"); clonedToAttach->accept(findBoneOffset); @@ -142,8 +144,6 @@ namespace SceneUtil trans = new osg::PositionAttitudeTransform; trans->setPosition(boneOffset->getMatrix().getTrans()); - // The BoneOffset rotation seems to be incorrect - trans->setAttitude(osg::Quat(osg::DegreesToRadians(-90.f), osg::Vec3f(1,0,0))); // Now that we used it, get rid of the redundant node. if (boneOffset->getNumChildren() == 0 && boneOffset->getNumParents() == 1) @@ -170,6 +170,13 @@ namespace SceneUtil trans->setStateSet(frontFaceStateSet); } + if(attitude) + { + if (!trans) + trans = new osg::PositionAttitudeTransform; + trans->setAttitude(*attitude); + } + if (trans) { attachNode->addChild(trans); diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index 806fc53488..ed0299dece 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -9,6 +9,11 @@ namespace osg { class Node; class Group; + class Quat; +} +namespace Resource +{ + class SceneManager; } namespace SceneUtil @@ -19,7 +24,7 @@ namespace SceneUtil /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode, Resource::SceneManager *sceneManager, const osg::Quat* attitude = nullptr); } diff --git a/components/sceneutil/clone.hpp b/components/sceneutil/clone.hpp index 1cf00c9e58..35240cbfba 100644 --- a/components/sceneutil/clone.hpp +++ b/components/sceneutil/clone.hpp @@ -18,6 +18,7 @@ namespace SceneUtil /// @par Defines the cloning behaviour we need: /// * Assigns updated ParticleSystem pointers on cloned emitters and programs. /// * Deep copies RigGeometry and MorphGeometry so they can animate without affecting clones. + /// @warning Avoid using this class directly. The safety of cloning operations depends on the copy flags and the objects involved. Consider using SceneManager::cloneNode for additional safety. /// @warning Do not use an object of this class for more than one copy operation. class CopyOp : public osg::CopyOp { diff --git a/components/sceneutil/controller.cpp b/components/sceneutil/controller.cpp index dfc72918aa..2c7507d0a3 100644 --- a/components/sceneutil/controller.cpp +++ b/components/sceneutil/controller.cpp @@ -121,6 +121,21 @@ namespace SceneUtil ctrl.setSource(mToAssign); } + ForceControllerSourcesVisitor::ForceControllerSourcesVisitor() + : AssignControllerSourcesVisitor() + { + } + + ForceControllerSourcesVisitor::ForceControllerSourcesVisitor(std::shared_ptr toAssign) + : AssignControllerSourcesVisitor(toAssign) + { + } + + void ForceControllerSourcesVisitor::visit(osg::Node&, Controller &ctrl) + { + ctrl.setSource(mToAssign); + } + FindMaxControllerLengthVisitor::FindMaxControllerLengthVisitor() : SceneUtil::ControllerVisitor() , mMaxLength(0) diff --git a/components/sceneutil/controller.hpp b/components/sceneutil/controller.hpp index 2656d654e1..6ef800f05b 100644 --- a/components/sceneutil/controller.hpp +++ b/components/sceneutil/controller.hpp @@ -85,10 +85,20 @@ namespace SceneUtil /// By default assigns the ControllerSource passed to the constructor of this class if no ControllerSource is assigned to that controller yet. void visit(osg::Node& node, Controller& ctrl) override; - private: + protected: std::shared_ptr mToAssign; }; + class ForceControllerSourcesVisitor : public AssignControllerSourcesVisitor + { + public: + ForceControllerSourcesVisitor(); + ForceControllerSourcesVisitor(std::shared_ptr toAssign); + + /// Assign the wanted ControllerSource even if one is already assigned to the controller. + void visit(osg::Node& node, Controller& ctrl) override; + }; + /// Finds the maximum of all controller functions in the given scene graph class FindMaxControllerLengthVisitor : public ControllerVisitor { diff --git a/components/sceneutil/depth.cpp b/components/sceneutil/depth.cpp new file mode 100644 index 0000000000..5d53b97726 --- /dev/null +++ b/components/sceneutil/depth.cpp @@ -0,0 +1,63 @@ +#include "depth.hpp" + +#include + +#include + +#include + +#ifndef GL_DEPTH32F_STENCIL8_NV +#define GL_DEPTH32F_STENCIL8_NV 0x8DAC +#endif + +namespace SceneUtil +{ + void setCameraClearDepth(osg::Camera* camera) + { + camera->setClearDepth(AutoDepth::isReversed() ? 0.0 : 1.0); + } + + osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near) + { + double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); + return osg::Matrix( + A/aspect, 0, 0, 0, + 0, A, 0, 0, + 0, 0, 0, -1, + 0, 0, near, 0 + ); + } + + osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far) + { + double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); + return osg::Matrix( + A/aspect, 0, 0, 0, + 0, A, 0, 0, + 0, 0, near/(far-near), -1, + 0, 0, (far*near)/(far - near), 0 + ); + } + + osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far) + { + return osg::Matrix( + 2/(right-left), 0, 0, 0, + 0, 2/(top-bottom), 0, 0, + 0, 0, 1/(far-near), 0, + (right+left)/(left-right), (top+bottom)/(bottom-top), far/(far-near), 1 + ); + } + + bool isFloatingPointDepthFormat(GLenum format) + { + constexpr std::array formats = { + GL_DEPTH_COMPONENT32F, + GL_DEPTH_COMPONENT32F_NV, + GL_DEPTH32F_STENCIL8, + GL_DEPTH32F_STENCIL8_NV, + }; + + return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); + } +} \ No newline at end of file diff --git a/components/sceneutil/depth.hpp b/components/sceneutil/depth.hpp new file mode 100644 index 0000000000..11a863ef7a --- /dev/null +++ b/components/sceneutil/depth.hpp @@ -0,0 +1,117 @@ +#ifndef OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H +#define OPENMW_COMPONENTS_SCENEUTIL_DEPTH_H + +#include + +#include "util.hpp" + +namespace SceneUtil +{ + // Sets camera clear depth to 0 if reversed depth buffer is in use, 1 otherwise. + void setCameraClearDepth(osg::Camera* camera); + + // Returns a perspective projection matrix for use with a reversed z-buffer + // and an infinite far plane. This is derived by mapping the default z-range + // of [0,1] to [1,0], then taking the limit as far plane approaches infinity. + osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near); + + // Returns a perspective projection matrix for use with a reversed z-buffer. + osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far); + + // Returns an orthographic projection matrix for use with a reversed z-buffer. + osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far); + + // Returns true if the GL format is a floating point depth format. + bool isFloatingPointDepthFormat(GLenum format); + + // Brief wrapper around an osg::Depth that applies the reversed depth function when a reversed depth buffer is in use + class AutoDepth : public osg::Depth + { + public: + AutoDepth(osg::Depth::Function func=osg::Depth::LESS, double zNear=0.0, double zFar=1.0, bool writeMask=true) + { + setFunction(func); + setZNear(zNear); + setZFar(zFar); + setWriteMask(writeMask); + } + + AutoDepth(const osg::Depth& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) : osg::Depth(copy, copyop) {} + + osg::Object* cloneType() const override { return new AutoDepth; } + osg::Object* clone(const osg::CopyOp& copyop) const override { return new AutoDepth(*this,copyop); } + + void apply(osg::State& state) const override + { + glDepthFunc(static_cast(AutoDepth::isReversed() ? getReversedDepthFunction() : getFunction())); + glDepthMask(static_cast(getWriteMask())); + #if defined(OSG_GLES1_AVAILABLE) || defined(OSG_GLES2_AVAILABLE) || defined(OSG_GLES3_AVAILABLE) + glDepthRangef(getZNear(),getZFar()); + #else + glDepthRange(getZNear(),getZFar()); + #endif + } + + static void setReversed(bool reverseZ) + { + static bool init = false; + + if (!init) + { + AutoDepth::sReversed = reverseZ; + init = true; + } + } + + static bool isReversed() + { + return AutoDepth::sReversed; + } + + private: + + static inline bool sReversed = false; + + osg::Depth::Function getReversedDepthFunction() const + { + const osg::Depth::Function func = getFunction(); + + switch (func) + { + case osg::Depth::LESS: + return osg::Depth::GREATER; + case osg::Depth::LEQUAL: + return osg::Depth::GEQUAL; + case osg::Depth::GREATER: + return osg::Depth::LESS; + case osg::Depth::GEQUAL: + return osg::Depth::LEQUAL; + default: + return func; + } + } + + }; + + // Replaces all nodes osg::Depth state attributes with SceneUtil::AutoDepth. + class ReplaceDepthVisitor : public osg::NodeVisitor + { + public: + ReplaceDepthVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) {} + + void apply(osg::Node& node) override + { + osg::StateSet* stateSet = node.getStateSet(); + + if (stateSet) + { + if (osg::Depth* depth = static_cast(stateSet->getAttribute(osg::StateAttribute::DEPTH))) + stateSet->setAttribute(new SceneUtil::AutoDepth(*depth)); + }; + + traverse(node); + } + }; +} + +#endif diff --git a/components/sceneutil/detourdebugdraw.cpp b/components/sceneutil/detourdebugdraw.cpp index 7ef329fc16..f1325340ea 100644 --- a/components/sceneutil/detourdebugdraw.cpp +++ b/components/sceneutil/detourdebugdraw.cpp @@ -32,19 +32,19 @@ namespace namespace SceneUtil { - DebugDraw::DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor) + DebugDraw::DebugDraw(osg::Group& group, const osg::ref_ptr& stateSet, + const osg::Vec3f& shift, float recastInvertedScaleFactor) : mGroup(group) + , mStateSet(stateSet) , mShift(shift) , mRecastInvertedScaleFactor(recastInvertedScaleFactor) - , mDepthMask(false) , mMode(osg::PrimitiveSet::POINTS) , mSize(1.0f) { } - void DebugDraw::depthMask(bool state) + void DebugDraw::depthMask(bool) { - mDepthMask = state; } void DebugDraw::texture(bool) @@ -56,7 +56,7 @@ namespace SceneUtil mMode = mode; mVertices = new osg::Vec3Array; mColors = new osg::Vec4Array; - mSize = size * mRecastInvertedScaleFactor; + mSize = size; } void DebugDraw::begin(duDebugDrawPrimitives prim, float size) @@ -88,16 +88,8 @@ namespace SceneUtil void DebugDraw::end() { - osg::ref_ptr stateSet(new osg::StateSet); - stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); - stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - stateSet->setMode(GL_DEPTH, (mDepthMask ? osg::StateAttribute::ON : osg::StateAttribute::OFF)); - stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - stateSet->setAttributeAndModes(new osg::LineWidth(mSize)); - stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - osg::ref_ptr geometry(new osg::Geometry); - geometry->setStateSet(stateSet); + geometry->setStateSet(mStateSet); geometry->setVertexArray(mVertices); geometry->setColorArray(mColors, osg::Array::BIND_PER_VERTEX); geometry->addPrimitiveSet(new osg::DrawArrays(mMode, 0, static_cast(mVertices->size()))); @@ -117,4 +109,16 @@ namespace SceneUtil { mColors->push_back(value); } + + osg::ref_ptr DebugDraw::makeStateSet() + { + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateSet->setMode(GL_DEPTH, osg::StateAttribute::OFF); + stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + stateSet->setAttributeAndModes(new osg::LineWidth()); + stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + return stateSet; + } } diff --git a/components/sceneutil/detourdebugdraw.hpp b/components/sceneutil/detourdebugdraw.hpp index 9b6a28acea..1d00735ea0 100644 --- a/components/sceneutil/detourdebugdraw.hpp +++ b/components/sceneutil/detourdebugdraw.hpp @@ -15,7 +15,10 @@ namespace SceneUtil class DebugDraw : public duDebugDraw { public: - DebugDraw(osg::Group& group, const osg::Vec3f& shift, float recastInvertedScaleFactor); + explicit DebugDraw(osg::Group& group, const osg::ref_ptr& stateSet, + const osg::Vec3f& shift, float recastInvertedScaleFactor); + + static osg::ref_ptr makeStateSet(); void depthMask(bool state) override; @@ -38,9 +41,9 @@ namespace SceneUtil private: osg::Group& mGroup; + osg::ref_ptr mStateSet; osg::Vec3f mShift; float mRecastInvertedScaleFactor; - bool mDepthMask; osg::PrimitiveSet::Mode mMode; float mSize; osg::ref_ptr mVertices; diff --git a/components/sceneutil/keyframe.hpp b/components/sceneutil/keyframe.hpp index 5be6924a09..59a87ab08e 100644 --- a/components/sceneutil/keyframe.hpp +++ b/components/sceneutil/keyframe.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include #include @@ -11,18 +11,20 @@ namespace SceneUtil { - class KeyframeController : public SceneUtil::Controller, public virtual osg::Callback + /// @note Derived classes are expected to derive from osg::Callback and implement getAsCallback(). + class KeyframeController : public SceneUtil::Controller, public virtual osg::Object { public: KeyframeController() {} KeyframeController(const KeyframeController& copy, const osg::CopyOp& copyop) - : osg::Callback(copy, copyop) - , SceneUtil::Controller(copy) - {} - META_Object(SceneUtil, KeyframeController) + : osg::Object(copy, copyop) + , SceneUtil::Controller(copy) {} virtual osg::Vec3f getTranslation(float time) const { return osg::Vec3f(); } + + /// @note We could drop this function in favour of osg::Object::asCallback from OSG 3.6 on. + virtual osg::Callback* getAsCallback() = 0; }; /// Wrapper object containing an animation track as a ref-countable osg::Object. diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index f38fd80d26..29907c542d 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1,6 +1,9 @@ #include "lightmanager.hpp" #include +#include +#include +#include #include #include @@ -26,18 +29,6 @@ namespace 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(); @@ -74,11 +65,6 @@ namespace mat(2, 3) = q; mat(3, 3) = r; } - - bool isReflectionCamera(osg::Camera* camera) - { - return (camera->getName() == "ReflectionCamera"); - } } namespace SceneUtil @@ -185,19 +171,15 @@ namespace SceneUtil void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) { - const Offsets offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride); + configureLayout(Offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride), size); + } - // Copy cloned data using current layout into current data using new layout. - // This allows to preserve osg::FloatArray buffer object in mData. - const auto data = mData->asVector(); - mData->resizeArray(static_cast(size)); - for (int i = 0; i < mCount; ++i) - { - std::memcpy(&(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f)); - std::memcpy(&(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f)); - std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f)); - } - mOffsets = offsets; + void configureLayout(const LightBuffer* other) + { + mOffsets = other->mOffsets; + int size = other->mData->size(); + + configureLayout(mOffsets, size); } private: @@ -239,6 +221,21 @@ namespace SceneUtil std::array mValues; }; + void configureLayout(const Offsets& offsets, int size) + { + // Copy cloned data using current layout into current data using new layout. + // This allows to preserve osg::FloatArray buffer object in mData. + const auto data = mData->asVector(); + mData->resizeArray(static_cast(size)); + for (int i = 0; i < mCount; ++i) + { + std::memcpy(&(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f)); + std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f)); + } + mOffsets = offsets; + } + osg::ref_ptr mData; osg::Endian mEndian; int mCount; @@ -246,9 +243,8 @@ namespace SceneUtil osg::Vec4 mCachedSunPos; }; - class LightStateCache + struct LightStateCache { - public: std::vector lastAppliedLight; }; @@ -278,9 +274,7 @@ namespace SceneUtil 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); + stateset->addUniform(lightManager->generateLightBufferUniform(lightMat), mode); break; } case LightingMethod::SingleUBO: @@ -315,25 +309,20 @@ namespace SceneUtil 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; } + META_StateAttribute(SceneUtil, DisableLight, osg::StateAttribute::LIGHT) unsigned int getMember() const override { return mIndex; } - bool getModeUsage(ModeUsage & usage) const override + bool getModeUsage(ModeUsage& usage) const override { usage.usesMode(GL_LIGHT0 + mIndex); return true; } - int compare(const StateAttribute &sa) const override + int compare(const StateAttribute& sa) const override { throw std::runtime_error("DisableLight::compare: unimplemented"); } @@ -358,7 +347,7 @@ namespace SceneUtil { public: FFPLightStateAttribute() : mIndex(0) {} - FFPLightStateAttribute(size_t index, const std::vector >& lights) : mIndex(index), mLights(lights) {} + 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) {} @@ -368,19 +357,19 @@ namespace SceneUtil return mIndex; } - bool getModeUsage(ModeUsage & usage) const override + 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 + int compare(const StateAttribute& sa) const override { throw std::runtime_error("FFPLightStateAttribute::compare: unimplemented"); } - META_StateAttribute(NifOsg, FFPLightStateAttribute, osg::StateAttribute::LIGHT) + META_StateAttribute(SceneUtil, FFPLightStateAttribute, osg::StateAttribute::LIGHT) void apply(osg::State& state) const override { @@ -426,69 +415,6 @@ namespace SceneUtil 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 - { - osg::StateSet* stateSet = mLightManager->getStateSet(); - if (!stateSet) - return; - - auto* lightUniform = stateSet->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; @@ -498,6 +424,8 @@ namespace SceneUtil virtual osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) = 0; virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {} + + osg::Matrix mViewMatrix; }; struct StateSetGeneratorFFP : StateSetGenerator @@ -585,37 +513,50 @@ namespace SceneUtil osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override { osg::ref_ptr stateset = new osg::StateSet; - - std::vector> lights(lightList.size()); + osg::ref_ptr data = mLightManager->generateLightBufferUniform(mLightManager->getSunlightBuffer(frameNum)); for (size_t i = 0; i < lightList.size(); ++i) { auto* light = lightList[i]->mLightSource->getLight(frameNum); - lights[i] = light; - setLightRadius(light, lightList[i]->mLightSource->getRadius()); - } + osg::Matrixf lightMat; + configurePosition(lightMat, light->getPosition() * mViewMatrix); + configureAmbient(lightMat, light->getAmbient()); + configureDiffuse(lightMat, light->getDiffuse()); + configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightList[i]->mLightSource->getRadius()); - stateset->setAttributeAndModes(new LightStateAttributePerObjectUniform(std::move(lights), mLightManager), osg::StateAttribute::ON); + data->setElement(i+1, lightMat); + } + stateset->addUniform(data); stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); return stateset; } }; + LightManager* findLightManager(const osg::NodePath& path) + { + for (size_t i = 0; i < path.size(); ++i) + { + if (LightManager* lightManager = dynamic_cast(path[i])) + return lightManager; + } + return nullptr; + } + // 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 SceneUtil::NodeCallback + class CollectLightCallback : public NodeCallback { public: CollectLightCallback() : mLightManager(nullptr) { } CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) - : SceneUtil::NodeCallback(copy, copyop) + : NodeCallback(copy, copyop) , mLightManager(nullptr) { } - META_Object(SceneUtil, SceneUtil::CollectLightCallback) + META_Object(SceneUtil, CollectLightCallback) void operator()(osg::Node* node, osg::NodeVisitor* nv) { @@ -653,169 +594,163 @@ namespace SceneUtil class LightManagerCullCallback : public SceneUtil::NodeCallback { public: - LightManagerCullCallback() : mLastFrameNumber(0) {} - void operator()(LightManager* node, osgUtil::CullVisitor* cv) { - if (mLastFrameNumber != cv->getTraversalNumber()) - { - mLastFrameNumber = cv->getTraversalNumber(); + osg::ref_ptr stateset = new osg::StateSet; - if (node->getLightingMethod() == LightingMethod::SingleUBO) - { - auto stateset = node->getStateSet(); - auto bo = node->getLightBuffer(mLastFrameNumber); + if (node->getLightingMethod() == LightingMethod::SingleUBO) + { + auto buffer = node->getUBOManager()->getLightBuffer(cv->getTraversalNumber()); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); #else - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData()->getBufferObject(), 0, buffer->getData()->getTotalDataSize()); #endif - stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); - } - - auto sun = node->getSunlight(); + stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); - if (sun) + if (auto sun = node->getSunlight()) { - // we must defer uploading the transformation to view-space position to deal with different cameras (e.g. reflection RTT). - if (node->getLightingMethod() == LightingMethod::PerObjectUniform) - { - osg::Matrixf lightMat; - configurePosition(lightMat, sun->getPosition()); - configureAmbient(lightMat, sun->getAmbient()); - configureDiffuse(lightMat, sun->getDiffuse()); - configureSpecular(lightMat, sun->getSpecular()); - node->setSunlightBuffer(lightMat, mLastFrameNumber); - } - else - { - auto buf = node->getLightBuffer(mLastFrameNumber); - - buf->setCachedSunPos(sun->getPosition()); - buf->setAmbient(0, sun->getAmbient()); - buf->setDiffuse(0, sun->getDiffuse()); - buf->setSpecular(0, sun->getSpecular()); - } + buffer->setCachedSunPos(sun->getPosition()); + buffer->setAmbient(0, sun->getAmbient()); + buffer->setDiffuse(0, sun->getDiffuse()); + buffer->setSpecular(0, sun->getSpecular()); + } + } + else if (node->getLightingMethod() == LightingMethod::PerObjectUniform) + { + if (auto sun = node->getSunlight()) + { + osg::Matrixf lightMat; + configurePosition(lightMat, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); + configureAmbient(lightMat, sun->getAmbient()); + configureDiffuse(lightMat, sun->getDiffuse()); + configureSpecular(lightMat, sun->getSpecular()); + node->setSunlightBuffer(lightMat, cv->getTraversalNumber()); + stateset->addUniform(node->generateLightBufferUniform(lightMat)); } } + cv->pushStateSet(stateset); traverse(node, cv); + cv->popStateSet(); } - - private: - size_t mLastFrameNumber; }; - class LightManagerStateAttribute : public osg::StateAttribute + UBOManager::UBOManager(int lightCount) + : mDummyProgram(new osg::Program) + , mInitLayout(false) + , mDirty({ true, true }) + , mTemplate(new LightBuffer(lightCount)) { - public: - LightManagerStateAttribute() - : mLightManager(nullptr) - , mInitLayout(false) - { - } + static const std::string dummyVertSource = generateDummyShader(lightCount); - LightManagerStateAttribute(LightManager* lightManager) - : mLightManager(lightManager) - , mDummyProgram(new osg::Program) - , mInitLayout(false) - { - static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene()); + // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably + // available, regardless of extensions, until GLSL 140. + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); - // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not reliably - // available, regardless of extensions, until GLSL 140. - mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); - mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); - } + for (size_t i = 0; i < mLightBuffers.size(); ++i) + { + mLightBuffers[i] = new LightBuffer(lightCount); - LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager), mInitLayout(copy.mInitLayout) {} + osg::ref_ptr ubo = new osg::UniformBufferObject; + ubo->setUsage(GL_STREAM_DRAW); - int compare(const StateAttribute &sa) const override - { - throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + mLightBuffers[i]->getData()->setBufferObject(ubo); } + } - META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT) + UBOManager::UBOManager(const UBOManager& copy, const osg::CopyOp& copyop) : osg::StateAttribute(copy,copyop), mDummyProgram(copy.mDummyProgram), mInitLayout(copy.mInitLayout) {} - void initSharedLayout(osg::GLExtensions* ext, int handle) const - { - constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; - int totalBlockSize = -1; - int stride = -1; + void UBOManager::releaseGLObjects(osg::State* state) const + { + mDummyProgram->releaseGLObjects(state); + } - ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); - ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); + int UBOManager::compare(const StateAttribute &sa) const + { + throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); + } - std::array names = { - "LightBuffer[0].packedColors", - "LightBuffer[0].position", - "LightBuffer[0].attenuation", - }; - std::vector indices(names.size()); - std::vector offsets(names.size()); + void UBOManager::apply(osg::State& state) const + { + unsigned int frame = state.getFrameStamp()->getFrameNumber(); + unsigned int index = frame % 2; - ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); - ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); + if (!mInitLayout) + { + mDummyProgram->apply(state); + auto handle = mDummyProgram->getPCP(state)->getHandle(); + auto* ext = state.get(); - for (int i = 0; i < 2; ++i) - mLightManager->getLightBuffer(i)->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); - } + int activeUniformBlocks = 0; + ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); - void apply(osg::State& state) const override - { - if (!mInitLayout) + // wait until the UBO binding is created + if (activeUniformBlocks > 0) { - mDummyProgram->apply(state); - auto handle = mDummyProgram->getPCP(state)->getHandle(); - auto* ext = state.get(); + initSharedLayout(ext, handle, frame); + mInitLayout = true; + } + } + else if (mDirty[index]) + { + mDirty[index] = false; + mLightBuffers[index]->configureLayout(mTemplate); + } - int activeUniformBlocks = 0; - ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); + mLightBuffers[index]->uploadCachedSunPos(state.getInitialViewMatrix()); + mLightBuffers[index]->dirty(); + } - // wait until the UBO binding is created - if (activeUniformBlocks > 0) - { - initSharedLayout(ext, handle); - mInitLayout = true; - } + std::string UBOManager::generateDummyShader(int maxLightsInScene) + { + const std::string define = "@maxLightsInScene"; + + std::string shader = R"GLSL( + #version 120 + #extension GL_ARB_uniform_buffer_object : require + struct LightData { + ivec4 packedColors; + vec4 position; + vec4 attenuation; + }; + uniform LightBufferBinding { + LightData LightBuffer[@maxLightsInScene]; + }; + void main() + { + gl_Position = vec4(0.0); } - mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->uploadCachedSunPos(state.getInitialViewMatrix()); - mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); - } + )GLSL"; - private: + shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); + return shader; + } - 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"; + void UBOManager::initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const + { + constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; + int totalBlockSize = -1; + int stride = -1; - shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); - return shader; - } + ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); + ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); - LightManager* mLightManager; - osg::ref_ptr mDummyProgram; - mutable bool mInitLayout; - }; + std::array names = { + "LightBuffer[0].packedColors", + "LightBuffer[0].position", + "LightBuffer[0].attenuation", + }; + std::vector indices(names.size()); + std::vector offsets(names.size()); + + ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); + ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); + + mTemplate->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } const std::unordered_map LightManager::mLightingMethodSettingMap = { {"legacy", LightingMethod::FFP} @@ -842,11 +777,6 @@ namespace SceneUtil return ""; } - LightManager::~LightManager() - { - getOrCreateStateSet()->removeAttribute(osg::StateAttribute::LIGHT); - } - LightManager::LightManager(bool ffp) : mStartLight(0) , mLightingMask(~0u) @@ -896,7 +826,7 @@ namespace SceneUtil getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); - addCullCallback(new LightManagerCullCallback()); + addCullCallback(new LightManagerCullCallback); } LightManager::LightManager(const LightManager ©, const osg::CopyOp ©op) @@ -967,55 +897,16 @@ namespace SceneUtil if (usingFFP()) return; - int targetLights = std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit); - setMaxLights(targetLights); + setMaxLights(std::clamp(Settings::Manager::getInt("max lights", "Shaders"), mMaxLightsLowerLimit, mMaxLightsUpperLimit)); if (getLightingMethod() == LightingMethod::PerObjectUniform) { - 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) - { - osg::Matrixf prevLightData; - prevUniform->getElement(i, prevLightData); - newUniform->setElement(i, prevLightData); - } - - 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(); - } + getStateSet()->removeUniform("LightBuffer"); + getStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); } - 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"); - - int prevCount; - uOldCount->get(prevCount); - int newCount = std::min(getMaxLights(), prevCount); - uOldCount->set(newCount); - osg::ref_ptr newArray = uOldArray->getIntArray(); - newArray->resize(newCount); - - stateset->removeUniform(uOldArray); - stateset->addUniform(new osg::Uniform("PointLightIndex", newArray)); - } - mStateSetCache[i].clear(); - } - } + for (auto& cache : mStateSetCache) + cache.clear(); } void LightManager::updateSettings() @@ -1044,14 +935,10 @@ namespace SceneUtil void LightManager::initPerObjectUniform(int targetLights) { - auto* stateset = getOrCreateStateSet(); - 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())); + getOrCreateStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); } void LightManager::initSingleUBO(int targetLights) @@ -1059,17 +946,8 @@ namespace SceneUtil setLightingMethod(LightingMethod::SingleUBO); setMaxLights(targetLights); - for (int i = 0; i < 2; ++i) - { - mLightBuffers[i] = new LightBuffer(getMaxLightsInScene()); - - osg::ref_ptr ubo = new osg::UniformBufferObject; - ubo->setUsage(GL_STREAM_DRAW); - - mLightBuffers[i]->getData()->setBufferObject(ubo); - } - - getOrCreateStateSet()->setAttribute(new LightManagerStateAttribute(this), osg::StateAttribute::ON); + mUBOManager = new UBOManager(getMaxLightsInScene()); + getOrCreateStateSet()->setAttributeAndModes(mUBOManager); } void LightManager::setLightingMethod(LightingMethod method) @@ -1158,29 +1036,45 @@ namespace SceneUtil return mSun; } - osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) + size_t LightManager::HashLightIdList::operator()(const LightIdList& lightIdList) const { - // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) size_t hash = 0; - for (size_t i = 0; i < lightList.size(); ++i) + for (size_t i = 0; i < lightIdList.size(); ++i) + Misc::hashCombine(hash, lightIdList[i]); + return hash; + } + + osg::ref_ptr LightManager::getLightListStateSet(const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) + { + if (getLightingMethod() == LightingMethod::PerObjectUniform) { - auto id = lightList[i]->mLightSource->getId(); - Misc::hashCombine(hash, id); + mStateSetGenerator->mViewMatrix = *viewMatrix; + return mStateSetGenerator->generate(lightList, frameNum); + } - if (getLightingMethod() != LightingMethod::SingleUBO) - continue; + // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a suitable one exists) - if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) - continue; + if (getLightingMethod() == LightingMethod::SingleUBO) + { + for (size_t i = 0; i < lightList.size(); ++i) + { + auto id = lightList[i]->mLightSource->getId(); + if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) + continue; - int index = getLightIndexMap(frameNum).size() + 1; - updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); - getLightIndexMap(frameNum).emplace(lightList[i]->mLightSource->getId(), index); + int index = getLightIndexMap(frameNum).size() + 1; + updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); + getLightIndexMap(frameNum).emplace(id, index); + } } auto& stateSetCache = mStateSetCache[frameNum%2]; - auto found = stateSetCache.find(hash); + LightIdList lightIdList; + lightIdList.reserve(lightList.size()); + std::transform(lightList.begin(), lightList.end(), std::back_inserter(lightIdList), [] (const LightSourceViewBound* l) { return l->mLightSource->getId(); }); + + auto found = stateSetCache.find(lightIdList); if (found != stateSetCache.end()) { mStateSetGenerator->update(found->second, lightList, frameNum); @@ -1188,11 +1082,11 @@ namespace SceneUtil } auto stateset = mStateSetGenerator->generate(lightList, frameNum); - stateSetCache.emplace(hash, stateset); + stateSetCache.emplace(lightIdList, stateset); return stateset; } - const std::vector& LightManager::getLightsInViewSpace(osgUtil::CullVisitor *cv, const osg::RefMatrix* viewMatrix, size_t frameNum) + const std::vector& LightManager::getLightsInViewSpace(osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum) { osg::Camera* camera = cv->getCurrentCamera(); @@ -1203,8 +1097,6 @@ namespace SceneUtil { it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; - bool isReflection = isReflectionCamera(camera); - for (const auto& transform : mLights) { osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); @@ -1214,7 +1106,7 @@ namespace SceneUtil osg::BoundingSphere viewBound = osg::BoundingSphere(osg::Vec3f(0,0,0), radius); transformBoundingSphere(worldViewMat, viewBound); - if (!isReflection && mPointLightFadeEnd != 0.f) + if (transform.mLightSource->getLastAppliedFrame() != frameNum && mPointLightFadeEnd != 0.f) { const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; float fade = 1 - std::clamp((viewBound.center().length() - mPointLightFadeStart) / fadeDelta, 0.f, 1.f); @@ -1223,6 +1115,7 @@ namespace SceneUtil auto* light = transform.mLightSource->getLight(frameNum); light->setDiffuse(light->getDiffuse() * fade); + transform.mLightSource->setLastAppliedFrame(frameNum); } // remove lights culled by this camera @@ -1259,16 +1152,25 @@ namespace SceneUtil void LightManager::updateGPUPointLight(int index, LightSource* lightSource, size_t frameNum,const osg::RefMatrix* viewMatrix) { auto* light = lightSource->getLight(frameNum); - auto& buf = getLightBuffer(frameNum); + auto& buf = getUBOManager()->getLightBuffer(frameNum); buf->setDiffuse(index, light->getDiffuse()); buf->setAmbient(index, light->getAmbient()); buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); buf->setPosition(index, light->getPosition() * (*viewMatrix)); } + osg::ref_ptr LightManager::generateLightBufferUniform(const osg::Matrixf& sun) + { + osg::ref_ptr uniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); + uniform->setElement(0, sun); + + return uniform; + } + LightSource::LightSource() : mRadius(0.f) , mActorFade(1.f) + , mLastAppliedFrame(0) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; @@ -1278,10 +1180,11 @@ namespace SceneUtil : osg::Node(copy, copyop) , mRadius(copy.mRadius) , mActorFade(copy.mActorFade) + , mLastAppliedFrame(copy.mLastAppliedFrame) { mId = sLightId++; - for (int i = 0; i < 2; ++i) + for (size_t i = 0; i < mLight.size(); ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } @@ -1345,7 +1248,7 @@ namespace SceneUtil { size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); - osg::StateSet* stateset = nullptr; + osg::ref_ptr stateset = nullptr; if (mLightList.size() > maxLights) { diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index b518a4723c..64b7e325ad 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -2,13 +2,11 @@ #define OPENMW_COMPONENTS_SCENEUTIL_LIGHTMANAGER_H #include -#include #include #include #include #include - #include #include #include @@ -46,7 +44,7 @@ namespace SceneUtil class LightSource : public osg::Node { // double buffered osg::Light's, since one of them may be in use by the draw thread at any given time - osg::ref_ptr mLight[2]; + std::array, 2> mLight; // LightSource will affect objects within this radius float mRadius; @@ -55,6 +53,8 @@ namespace SceneUtil float mActorFade; + unsigned int mLastAppliedFrame; + public: META_Node(SceneUtil, LightSource) @@ -107,6 +107,43 @@ namespace SceneUtil { return mId; } + + void setLastAppliedFrame(unsigned int lastAppliedFrame) + { + mLastAppliedFrame = lastAppliedFrame; + } + + unsigned int getLastAppliedFrame() const + { + return mLastAppliedFrame; + } + }; + + class UBOManager : public osg::StateAttribute + { + public: + UBOManager(int lightCount=1); + UBOManager(const UBOManager& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY); + + void releaseGLObjects(osg::State* state) const override; + + int compare(const StateAttribute& sa) const override; + + META_StateAttribute(SceneUtil, UBOManager, osg::StateAttribute::LIGHT) + + void apply(osg::State& state) const override; + + auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + + private: + std::string generateDummyShader(int maxLightsInScene); + void initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const; + + osg::ref_ptr mDummyProgram; + mutable bool mInitLayout; + mutable std::array, 2> mLightBuffers; + mutable std::array mDirty; + osg::ref_ptr mTemplate; }; /// @brief Decorator node implementing the rendering of any number of LightSources that can be anywhere in the subgraph. @@ -138,8 +175,6 @@ namespace SceneUtil LightManager(const LightManager& copy, const osg::CopyOp& copyop); - ~LightManager(); - /// @param mask This mask is compared with the current Camera's cull mask to determine if lighting is desired. /// 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 @@ -176,7 +211,7 @@ namespace SceneUtil auto& getLightIndexMap(size_t frameNum) { return mLightIndexMaps[frameNum%2]; } - auto& getLightBuffer(size_t frameNum) { return mLightBuffers[frameNum%2]; } + auto& getUBOManager() { return mUBOManager; } osg::Matrixf getSunlightBuffer(size_t frameNum) const { return mSunlightBuffers[frameNum%2]; } void setSunlightBuffer(const osg::Matrixf& buffer, size_t frameNum) { mSunlightBuffers[frameNum%2] = buffer; } @@ -190,6 +225,8 @@ namespace SceneUtil /// Not thread safe, it is the responsibility of the caller to stop/start threading on the viewer void updateMaxLights(); + osg::ref_ptr generateLightBufferUniform(const osg::Matrixf& sun); + private: void initFFP(int targetLights); void initPerObjectUniform(int targetLights); @@ -207,8 +244,12 @@ namespace SceneUtil using LightSourceViewBoundCollection = std::vector; std::map, LightSourceViewBoundCollection> mLightsInViewSpace; - // < Light list hash , StateSet > - using LightStateSetMap = std::map>; + using LightIdList = std::vector; + struct HashLightIdList + { + size_t operator()(const LightIdList&) const; + }; + using LightStateSetMap = std::unordered_map, HashLightIdList>; LightStateSetMap mStateSetCache[2]; std::vector> mDummies; @@ -219,8 +260,6 @@ namespace SceneUtil osg::ref_ptr mSun; - osg::ref_ptr mLightBuffers[2]; - osg::Matrixf mSunlightBuffers[2]; // < Light ID , Buffer Index > @@ -229,6 +268,8 @@ namespace SceneUtil std::unique_ptr mStateSetGenerator; + osg::ref_ptr mUBOManager; + LightingMethod mLightingMethod; float mPointLightRadiusMultiplier; diff --git a/components/sceneutil/lightutil.cpp b/components/sceneutil/lightutil.cpp index 6a1a1376ec..2a5a945558 100644 --- a/components/sceneutil/lightutil.cpp +++ b/components/sceneutil/lightutil.cpp @@ -79,6 +79,7 @@ namespace SceneUtil // PositionAttitudeTransform seems to be slightly faster than MatrixTransform osg::ref_ptr trans(new SceneUtil::PositionAttitudeTransform); trans->setPosition(computeBound.getBoundingBox().center()); + trans->setNodeMask(lightMask); node->addChild(trans); diff --git a/components/sceneutil/morphgeometry.cpp b/components/sceneutil/morphgeometry.cpp index 04fd6fb365..3e34e3dedc 100644 --- a/components/sceneutil/morphgeometry.cpp +++ b/components/sceneutil/morphgeometry.cpp @@ -1,6 +1,7 @@ #include "morphgeometry.hpp" #include +#include #include @@ -27,11 +28,19 @@ MorphGeometry::MorphGeometry(const MorphGeometry ©, const osg::CopyOp ©o void MorphGeometry::setSourceGeometry(osg::ref_ptr sourceGeom) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeom; for (unsigned int i=0; i<2; ++i) { + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by TemplateRef) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(*mSourceGeometry, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); const osg::Geometry& from = *mSourceGeometry; osg::Geometry& to = *mGeometry[i]; @@ -95,8 +104,8 @@ void MorphGeometry::accept(osg::PrimitiveFunctor& func) const osg::BoundingBox MorphGeometry::computeBoundingBox() const { bool anyMorphTarget = false; - for (unsigned int i=0; i 0) + for (unsigned int i=1; i(mSourceGeometry->getVertexArray()); - std::vector vertBounds(sourceVerts.size()); + const osg::Vec3Array* sourceVerts = static_cast(mSourceGeometry->getVertexArray()); + if (mMorphTargets.size() != 0) + sourceVerts = mMorphTargets[0].getOffsets(); + std::vector vertBounds(sourceVerts->size()); // Since we don't know what combinations of morphs are being applied we need to keep track of a bounding box for each vertex. // The minimum/maximum of the box is the minimum/maximum offset the vertex can have from its starting position. @@ -123,7 +134,7 @@ osg::BoundingBox MorphGeometry::computeBoundingBox() const for (unsigned int i=0; igetTraversalNumber() || !mDirty) + if (mLastFrameNumber == nv->getTraversalNumber() || !mDirty || mMorphTargets.size() == 0) { osg::Geometry& geom = *getGeometry(mLastFrameNumber); nv->pushOntoNodePath(&geom); @@ -160,13 +171,13 @@ void MorphGeometry::cull(osg::NodeVisitor *nv) mLastFrameNumber = nv->getTraversalNumber(); osg::Geometry& geom = *getGeometry(mLastFrameNumber); - const osg::Vec3Array* positionSrc = static_cast(mSourceGeometry->getVertexArray()); + const osg::Vec3Array* positionSrc = mMorphTargets[0].getOffsets(); osg::Vec3Array* positionDst = static_cast(geom.getVertexArray()); assert(positionSrc->size() == positionDst->size()); for (unsigned int vertex=0; vertexsize(); ++vertex) (*positionDst)[vertex] = (*positionSrc)[vertex]; - for (unsigned int i=0; idirty(); -#if OSG_MIN_VERSION_REQUIRED(3, 5, 6) - geom.dirtyGLObjects(); +#if OSG_MIN_VERSION_REQUIRED(3, 5, 10) + geom.osg::Drawable::dirtyGLObjects(); #endif nv->pushOntoNodePath(&geom); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index db7aef7cb1..daf6bb80ab 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -27,8 +27,6 @@ #include -#include - #include "shadowsbin.hpp" namespace { @@ -279,14 +277,7 @@ void VDSMCameraCullCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) } #endif // bin has to go inside camera cull or the rendertexture stage will override it - static osg::ref_ptr ss; - if (!ss) - { - ShadowsBinAdder adder("ShadowsBin", _vdsm->getCastingPrograms()); - ss = new osg::StateSet; - ss->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, "ShadowsBin", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS); - } - cv->pushStateSet(ss); + cv->pushStateSet(_vdsm->getOrCreateShadowsBinStateSet()); if (_vdsm->getShadowedScene()) { _vdsm->getShadowedScene()->osg::Group::traverse(*nv); @@ -357,8 +348,6 @@ MWShadowTechnique::ComputeLightSpaceBounds::ComputeLightSpaceBounds() : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN) { setCullingMode(osg::CullSettings::VIEW_FRUSTUM_CULLING); - - setName("SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds,AcceptedByComponentsTerrainQuadTreeWorld"); } void MWShadowTechnique::ComputeLightSpaceBounds::reset() @@ -380,6 +369,11 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Node& node) popCurrentMask(); } +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Group& node) +{ + apply(static_cast(node)); +} + void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Drawable& drawable) { if (isCulled(drawable)) return; @@ -393,12 +387,9 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Drawable& drawable) popCurrentMask(); } -void MWShadowTechnique::ComputeLightSpaceBounds::apply(Terrain::QuadTreeWorld & quadTreeWorld) +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Geometry& drawable) { - // For now, just expand the bounds fully as terrain will fill them up and possible ways to detect which terrain definitely won't cast shadows aren't implemented. - - update(osg::Vec3(-1.0, -1.0, 0.0)); - update(osg::Vec3(1.0, 1.0, 0.0)); + apply(static_cast(drawable)); } void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Billboard&) @@ -434,7 +425,11 @@ void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Transform& transform // pop the culling mode. popCurrentMask(); +} +void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::MatrixTransform& transform) +{ + apply(static_cast(transform)); } void MWShadowTechnique::ComputeLightSpaceBounds::apply(osg::Camera&) @@ -807,6 +802,8 @@ MWShadowTechnique::MWShadowTechnique(const MWShadowTechnique& vdsm, const osg::C MWShadowTechnique::~MWShadowTechnique() { + if (_shadowsBin != nullptr) + osgUtil::RenderBin::removeRenderBinPrototype(_shadowsBin); } @@ -1122,7 +1119,7 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) // if we are using multiple shadow maps and CastShadowTraversalMask is being used // traverse the scene to compute the extents of the objects - if (/*numShadowMapsPerLight>1 &&*/ _shadowedScene->getCastsShadowTraversalMask()!=0xffffffff) + if (/*numShadowMapsPerLight>1 &&*/ (_shadowedScene->getCastsShadowTraversalMask() & _worldMask) == 0) { // osg::ElapsedTime timer; @@ -1653,11 +1650,8 @@ void MWShadowTechnique::createShaders() _shadowCastingStateSet->addUniform(new osg::Uniform("alphaTestShadows", false)); osg::ref_ptr depth = new osg::Depth; depth->setWriteMask(true); - if (SceneUtil::getReverseZ()) - { - osg::ref_ptr clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::NEGATIVE_ONE_TO_ONE); - _shadowCastingStateSet->setAttribute(clipcontrol, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); - } + osg::ref_ptr clipcontrol = new osg::ClipControl(osg::ClipControl::LOWER_LEFT, osg::ClipControl::NEGATIVE_ONE_TO_ONE); + _shadowCastingStateSet->setAttribute(clipcontrol, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); _shadowCastingStateSet->setAttribute(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); _shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON); @@ -3278,3 +3272,18 @@ void SceneUtil::MWShadowTechnique::DebugHUD::addAnotherShadowMap() for(auto& uniformVector : mFrustumUniforms) uniformVector.push_back(new osg::Uniform(osg::Uniform::FLOAT_MAT4, "transform")); } + +osg::ref_ptr SceneUtil::MWShadowTechnique::getOrCreateShadowsBinStateSet() +{ + if (_shadowsBinStateSet == nullptr) + { + if (_shadowsBin == nullptr) + { + _shadowsBin = new ShadowsBin(_castingPrograms); + osgUtil::RenderBin::addRenderBinPrototype(_shadowsBinName, _shadowsBin); + } + _shadowsBinStateSet = new osg::StateSet; + _shadowsBinStateSet->setRenderBinDetails(osg::StateSet::OPAQUE_BIN, _shadowsBinName, osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS); + } + return _shadowsBinStateSet; +} diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp index 229d444c2b..574d054204 100644 --- a/components/sceneutil/mwshadowtechnique.hpp +++ b/components/sceneutil/mwshadowtechnique.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -31,7 +32,6 @@ #include #include -#include namespace SceneUtil { @@ -92,22 +92,21 @@ namespace SceneUtil { public: ComputeLightSpaceBounds(); - void apply(osg::Node& node) override; + void apply(osg::Node& node) override final; + void apply(osg::Group& node) override; - void apply(osg::Drawable& drawable) override; - - void apply(Terrain::QuadTreeWorld& quadTreeWorld); + void apply(osg::Drawable& drawable) override final; + void apply(osg::Geometry& drawable) override; void apply(osg::Billboard&) override; void apply(osg::Projection&) override; - void apply(osg::Transform& transform) override; + void apply(osg::Transform& transform) override final; + void apply(osg::MatrixTransform& transform) override; void apply(osg::Camera&) override; - using osg::NodeVisitor::apply; - void updateBound(const osg::BoundingBox& bb); void update(const osg::Vec3& v); @@ -217,8 +216,6 @@ namespace SceneUtil { virtual void createShaders(); - virtual std::array, GL_ALWAYS - GL_NEVER + 1> getCastingPrograms() const { return _castingPrograms; } - virtual bool selectActiveLights(osgUtil::CullVisitor* cv, ViewDependentData* vdd) const; virtual osg::Polytope computeLightViewFrustumPolytope(Frustum& frustum, LightData& positionedLight); @@ -237,6 +234,10 @@ namespace SceneUtil { virtual osg::StateSet* prepareStateSetForRenderingShadow(ViewDependentData& vdd, unsigned int traversalNumber) const; + void setWorldMask(unsigned int worldMask) { _worldMask = worldMask; } + + osg::ref_ptr getOrCreateShadowsBinStateSet(); + protected: virtual ~MWShadowTechnique(); @@ -270,6 +271,8 @@ namespace SceneUtil { float _shadowFadeStart = 0.0; + unsigned int _worldMask = ~0u; + class DebugHUD final : public osg::Referenced { public: @@ -295,6 +298,9 @@ namespace SceneUtil { osg::ref_ptr _debugHud; std::array, GL_ALWAYS - GL_NEVER + 1> _castingPrograms; + const std::string _shadowsBinName = "ShadowsBin_" + std::to_string(reinterpret_cast(this)); + osg::ref_ptr _shadowsBin; + osg::ref_ptr _shadowsBinStateSet; }; } diff --git a/components/sceneutil/navmesh.cpp b/components/sceneutil/navmesh.cpp index b0f356f089..d66f95381c 100644 --- a/components/sceneutil/navmesh.cpp +++ b/components/sceneutil/navmesh.cpp @@ -1,5 +1,6 @@ #include "navmesh.hpp" #include "detourdebugdraw.hpp" +#include "depth.hpp" #include @@ -7,21 +8,256 @@ #include #include +#include -namespace SceneUtil +namespace { - osg::ref_ptr createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings) + // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L26-L38 + float distancePtLine2d(const float* pt, const float* p, const float* q) { - const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 10), 1.0f / settings.mRecastScaleFactor); - dtNavMeshQuery navMeshQuery; - navMeshQuery.init(&navMesh, settings.mMaxNavMeshQueryNodes); - duDebugDrawNavMeshWithClosedList(&debugDraw, navMesh, navMeshQuery, - DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST); + float pqx = q[0] - p[0]; + float pqz = q[2] - p[2]; + float dx = pt[0] - p[0]; + float dz = pt[2] - p[2]; + float d = pqx*pqx + pqz*pqz; + float t = pqx*dx + pqz*dz; + if (d != 0) t /= d; + dx = p[0] + t*pqx - pt[0]; + dz = p[2] + t*pqz - pt[2]; + return dx*dx + dz*dz; + } + + // Copied from https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L40-L118 + void drawPolyBoundaries(duDebugDraw* dd, const dtMeshTile* tile, const unsigned int col, + const float linew, bool inner) + { + static const float thr = 0.01f*0.01f; + + dd->begin(DU_DRAW_LINES, linew); + + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + + if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) continue; + + const dtPolyDetail* pd = &tile->detailMeshes[i]; + + for (int j = 0, nj = (int)p->vertCount; j < nj; ++j) + { + unsigned int c = col; + if (inner) + { + if (p->neis[j] == 0) continue; + if (p->neis[j] & DT_EXT_LINK) + { + bool con = false; + for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next) + { + if (tile->links[k].edge == j) + { + con = true; + break; + } + } + if (con) + c = duRGBA(255,255,255,48); + else + c = duRGBA(0,0,0,48); + } + else + c = duRGBA(0,48,64,32); + } + else + { + if (p->neis[j] != 0) continue; + } + + const float* v0 = &tile->verts[p->verts[j]*3]; + const float* v1 = &tile->verts[p->verts[(j+1) % nj]*3]; + + // Draw detail mesh edges which align with the actual poly edge. + // This is really slow. + for (int k = 0; k < pd->triCount; ++k) + { + const unsigned char* t = &tile->detailTris[(pd->triBase+k)*4]; + const float* tv[3]; + for (int m = 0; m < 3; ++m) + { + if (t[m] < p->vertCount) + tv[m] = &tile->verts[p->verts[t[m]]*3]; + else + tv[m] = &tile->detailVerts[(pd->vertBase+(t[m]-p->vertCount))*3]; + } + for (int m = 0, n = 2; m < 3; n=m++) + { + if ((dtGetDetailTriEdgeFlags(t[3], n) & DT_DETAIL_EDGE_BOUNDARY) == 0) + continue; + + if (distancePtLine2d(tv[n],v0,v1) < thr && + distancePtLine2d(tv[m],v0,v1) < thr) + { + dd->vertex(tv[n], c); + dd->vertex(tv[m], c); + } + } + } + } + } + dd->end(); + } + + // Based on https://github.com/recastnavigation/recastnavigation/blob/c5cbd53024c8a9d8d097a4371215e3342d2fdc87/DebugUtils/Source/DetourDebugDraw.cpp#L120-L235 + void drawMeshTile(duDebugDraw* dd, const dtNavMesh& mesh, const dtNavMeshQuery* query, + const dtMeshTile* tile, unsigned char flags) + { + dtPolyRef base = mesh.getPolyRefBase(tile); + + int tileNum = mesh.decodePolyIdTile(base); + const unsigned int tileNumColor = duIntToCol(tileNum, 128); + const unsigned alpha = tile->header->userId == 0 ? 64 : 128; + + dd->depthMask(false); + + dd->begin(DU_DRAW_TRIS); + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + if (p->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) // Skip off-mesh links. + continue; + + const dtPolyDetail* pd = &tile->detailMeshes[i]; + + unsigned int col; + if (query && query->isInClosedList(base | (dtPolyRef)i)) + col = duRGBA(255, 196, 0, alpha); + else + { + if (flags & DU_DRAWNAVMESH_COLOR_TILES) + col = duTransCol(tileNumColor, alpha); + else + col = duTransCol(dd->areaToCol(p->getArea()), alpha); + } + + for (int j = 0; j < pd->triCount; ++j) + { + const unsigned char* t = &tile->detailTris[(pd->triBase+j)*4]; + for (int k = 0; k < 3; ++k) + { + if (t[k] < p->vertCount) + dd->vertex(&tile->verts[p->verts[t[k]]*3], col); + else + dd->vertex(&tile->detailVerts[(pd->vertBase+t[k]-p->vertCount)*3], col); + } + } + } + dd->end(); + + // Draw inter poly boundaries + drawPolyBoundaries(dd, tile, duRGBA(0,48,64,32), 1.5f, true); + + // Draw outer poly boundaries + drawPolyBoundaries(dd, tile, duRGBA(0,48,64,220), 2.5f, false); + if (flags & DU_DRAWNAVMESH_OFFMESHCONS) + { + dd->begin(DU_DRAW_LINES, 2.0f); + for (int i = 0; i < tile->header->polyCount; ++i) + { + const dtPoly* p = &tile->polys[i]; + if (p->getType() != DT_POLYTYPE_OFFMESH_CONNECTION) // Skip regular polys. + continue; + + unsigned int col, col2; + if (query && query->isInClosedList(base | (dtPolyRef)i)) + col = duRGBA(255,196,0,220); + else + col = duDarkenCol(duTransCol(dd->areaToCol(p->getArea()), 220)); + + const dtOffMeshConnection* con = &tile->offMeshCons[i - tile->header->offMeshBase]; + const float* va = &tile->verts[p->verts[0]*3]; + const float* vb = &tile->verts[p->verts[1]*3]; + + // Check to see if start and end end-points have links. + bool startSet = false; + bool endSet = false; + for (unsigned int k = p->firstLink; k != DT_NULL_LINK; k = tile->links[k].next) + { + if (tile->links[k].edge == 0) + startSet = true; + if (tile->links[k].edge == 1) + endSet = true; + } + + // End points and their on-mesh locations. + dd->vertex(va[0],va[1],va[2], col); + dd->vertex(con->pos[0],con->pos[1],con->pos[2], col); + col2 = startSet ? col : duRGBA(220,32,16,196); + duAppendCircle(dd, con->pos[0],con->pos[1]+0.1f,con->pos[2], con->rad, col2); + + dd->vertex(vb[0],vb[1],vb[2], col); + dd->vertex(con->pos[3],con->pos[4],con->pos[5], col); + col2 = endSet ? col : duRGBA(220,32,16,196); + duAppendCircle(dd, con->pos[3],con->pos[4]+0.1f,con->pos[5], con->rad, col2); + + // End point vertices. + dd->vertex(con->pos[0],con->pos[1],con->pos[2], duRGBA(0,48,64,196)); + dd->vertex(con->pos[0],con->pos[1]+0.2f,con->pos[2], duRGBA(0,48,64,196)); + + dd->vertex(con->pos[3],con->pos[4],con->pos[5], duRGBA(0,48,64,196)); + dd->vertex(con->pos[3],con->pos[4]+0.2f,con->pos[5], duRGBA(0,48,64,196)); + + // Connection arc. + duAppendArc(dd, con->pos[0],con->pos[1],con->pos[2], con->pos[3],con->pos[4],con->pos[5], 0.25f, + (con->flags & 1) ? 0.6f : 0, 0.6f, col); + } + dd->end(); + } + + const unsigned int vcol = duRGBA(0,0,0,196); + dd->begin(DU_DRAW_POINTS, 3.0f); + for (int i = 0; i < tile->header->vertCount; ++i) + { + const float* v = &tile->verts[i*3]; + dd->vertex(v[0], v[1], v[2], vcol); + } + dd->end(); + + dd->depthMask(true); + } +} + +namespace SceneUtil +{ + osg::ref_ptr makeNavMeshTileStateSet() + { osg::ref_ptr material = new osg::Material; material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - group->getOrCreateStateSet()->setAttribute(material); + + const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + osg::ref_ptr polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits); + + osg::ref_ptr stateSet = new osg::StateSet; + stateSet->setAttribute(material); + stateSet->setAttributeAndModes(polygonOffset); + return stateSet; + } + + osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, + const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, + const osg::ref_ptr& debugDrawStateSet) + { + if (meshTile.header == nullptr) + return nullptr; + + osg::ref_ptr group(new osg::Group); + group->setStateSet(groupStateSet); + constexpr float shift = 10.0f; + DebugDraw debugDraw(*group, debugDrawStateSet, osg::Vec3f(0, 0, shift), 1.0f / settings.mRecastScaleFactor); + dtNavMeshQuery navMeshQuery; + navMeshQuery.init(&navMesh, settings.mMaxNavMeshQueryNodes); + drawMeshTile(&debugDraw, navMesh, &navMeshQuery, &meshTile, DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST); return group; } diff --git a/components/sceneutil/navmesh.hpp b/components/sceneutil/navmesh.hpp index b255b05756..ca9b8bd570 100644 --- a/components/sceneutil/navmesh.hpp +++ b/components/sceneutil/navmesh.hpp @@ -4,10 +4,12 @@ #include class dtNavMesh; +struct dtMeshTile; namespace osg { class Group; + class StateSet; } namespace DetourNavigator @@ -17,7 +19,11 @@ namespace DetourNavigator namespace SceneUtil { - osg::ref_ptr createNavMeshGroup(const dtNavMesh& navMesh, const DetourNavigator::Settings& settings); + osg::ref_ptr makeNavMeshTileStateSet(); + + osg::ref_ptr createNavMeshTileGroup(const dtNavMesh& navMesh, const dtMeshTile& meshTile, + const DetourNavigator::Settings& settings, const osg::ref_ptr& groupStateSet, + const osg::ref_ptr& debugDrawStateSet); } #endif diff --git a/components/sceneutil/nodecallback.hpp b/components/sceneutil/nodecallback.hpp index 6f0140d64c..96e3ae229e 100644 --- a/components/sceneutil/nodecallback.hpp +++ b/components/sceneutil/nodecallback.hpp @@ -13,13 +13,12 @@ namespace SceneUtil { template -class NodeCallback : public virtual osg::Callback +class NodeCallback : public osg::Callback { public: NodeCallback(){} NodeCallback(const NodeCallback& nc,const osg::CopyOp& copyop): osg::Callback(nc, copyop) {} - META_Object(SceneUtil, NodeCallback) bool run(osg::Object* object, osg::Object* data) override { diff --git a/components/sceneutil/optimizer.cpp b/components/sceneutil/optimizer.cpp index 2cfabbfe19..748ceee952 100644 --- a/components/sceneutil/optimizer.cpp +++ b/components/sceneutil/optimizer.cpp @@ -42,7 +42,7 @@ #include -#include +#include using namespace osgUtil; @@ -171,7 +171,7 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor setTraversalMode(osg::NodeVisitor::TRAVERSE_PARENTS); } - void apply(osg::Node& node) override + void apply(osg::Group& node) override { if (node.getNumParents()) { @@ -180,7 +180,7 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor else { // for all current objects mark a nullptr transform for them. - registerWithCurrentObjects(0); + registerWithCurrentObjects(static_cast(nullptr)); } } @@ -198,15 +198,19 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor // for all current objects associated this transform with them. registerWithCurrentObjects(&transform); } + void apply(osg::MatrixTransform& transform) override + { + // for all current objects associated this transform with them. + registerWithCurrentObjects(&transform); + } - void apply(osg::Geode& geode) override + void apply(osg::Node& node) override { - traverse(geode); + traverse(node); } - void apply(osg::Billboard& geode) override + void apply(osg::Geometry& geode) override { - traverse(geode); } void collectDataFor(osg::Node* node) @@ -293,7 +297,19 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor ObjectStruct():_canBeApplied(true),_moreThanOneMatrixRequired(false) {} - void add(osg::Transform* transform, bool canOptimize) + inline const osg::Matrix& getMatrix(osg::MatrixTransform* transform) + { + return transform->getMatrix(); + } + osg::Matrix getMatrix(osg::Transform* transform) + { + osg::Matrix matrix; + transform->computeLocalToWorldMatrix(matrix, 0); + return matrix; + } + + template + void add(T* transform, bool canOptimize) { if (transform) { @@ -301,12 +317,10 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor else if (transform->getReferenceFrame()!=osg::Transform::RELATIVE_RF) _moreThanOneMatrixRequired=true; else { - if (_transformSet.empty()) transform->computeLocalToWorldMatrix(_firstMatrix,0); + if (_transformSet.empty()) _firstMatrix = getMatrix(transform); else { - osg::Matrix matrix; - transform->computeLocalToWorldMatrix(matrix,0); - if (_firstMatrix!=matrix) _moreThanOneMatrixRequired=true; + if (_firstMatrix!=getMatrix(transform)) _moreThanOneMatrixRequired=true; } } } @@ -327,8 +341,8 @@ class CollectLowestTransformsVisitor : public BaseOptimizerVisitor TransformSet _transformSet; }; - - void registerWithCurrentObjects(osg::Transform* transform) + template + void registerWithCurrentObjects(T* transform) { for(ObjectList::iterator itr=_currentObjectList.begin(); itr!=_currentObjectList.end(); @@ -633,19 +647,23 @@ osg::Array* cloneArray(osg::Array* array, osg::VertexBufferObject*& vbo, const o return array; } -void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Drawable& drawable) +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Geometry& geometry) { - osg::Geometry *geometry = drawable.asGeometry(); - if((geometry) && (isOperationPermissibleForObject(&drawable))) + if(isOperationPermissibleForObject(&geometry)) { osg::VertexBufferObject* vbo = nullptr; - if(geometry->getVertexArray() && geometry->getVertexArray()->referenceCount() > 1) - geometry->setVertexArray(cloneArray(geometry->getVertexArray(), vbo, geometry)); - if(geometry->getNormalArray() && geometry->getNormalArray()->referenceCount() > 1) - geometry->setNormalArray(cloneArray(geometry->getNormalArray(), vbo, geometry)); - if(geometry->getTexCoordArray(7) && geometry->getTexCoordArray(7)->referenceCount() > 1) // tangents - geometry->setTexCoordArray(7, cloneArray(geometry->getTexCoordArray(7), vbo, geometry)); + if(geometry.getVertexArray() && geometry.getVertexArray()->referenceCount() > 1) + geometry.setVertexArray(cloneArray(geometry.getVertexArray(), vbo, &geometry)); + if(geometry.getNormalArray() && geometry.getNormalArray()->referenceCount() > 1) + geometry.setNormalArray(cloneArray(geometry.getNormalArray(), vbo, &geometry)); + if(geometry.getTexCoordArray(7) && geometry.getTexCoordArray(7)->referenceCount() > 1) // tangents + geometry.setTexCoordArray(7, cloneArray(geometry.getTexCoordArray(7), vbo, &geometry)); } + _drawableSet.insert(&geometry); +} + +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Drawable& drawable) +{ _drawableSet.insert(&drawable); } @@ -673,6 +691,11 @@ void Optimizer::FlattenStaticTransformsVisitor::apply(osg::Transform& transform) _transformStack.pop_back(); } +void Optimizer::FlattenStaticTransformsVisitor::apply(osg::MatrixTransform& transform) +{ + apply(static_cast(transform)); +} + bool Optimizer::FlattenStaticTransformsVisitor::removeTransforms(osg::Node* nodeWeCannotRemove) { CollectLowestTransformsVisitor cltv(_optimizer); @@ -1112,10 +1135,13 @@ bool isAbleToMerge(const osg::Geometry& g1, const osg::Geometry& g2) } -void Optimizer::MergeGeometryVisitor::pushStateSet(osg::StateSet *stateSet) +bool Optimizer::MergeGeometryVisitor::pushStateSet(osg::StateSet *stateSet) { + if (!stateSet || stateSet->getRenderBinMode() & osg::StateSet::INHERIT_RENDERBIN_DETAILS) + return false; _stateSetStack.push_back(stateSet); checkAlphaBlendingActive(); + return true; } void Optimizer::MergeGeometryVisitor::popStateSet() @@ -1145,15 +1171,14 @@ void Optimizer::MergeGeometryVisitor::checkAlphaBlendingActive() void Optimizer::MergeGeometryVisitor::apply(osg::Group &group) { - if (group.getStateSet()) - pushStateSet(group.getStateSet()); + bool pushed = pushStateSet(group.getStateSet()); if (!_alphaBlendingActive || _mergeAlphaBlending) mergeGroup(group); traverse(group); - if (group.getStateSet()) + if (pushed) popStateSet(); } @@ -1572,8 +1597,8 @@ bool Optimizer::MergeGeometryVisitor::mergeGroup(osg::Group& group) } if (_alphaBlendingActive && _mergeAlphaBlending && !geom->getStateSet()) { - auto d = createDepth(); - d->setWriteMask(0); + osg::ref_ptr d = new SceneUtil::AutoDepth; + d->setWriteMask(false); geom->getOrCreateStateSet()->setAttribute(d); } } diff --git a/components/sceneutil/optimizer.hpp b/components/sceneutil/optimizer.hpp index 7b32bafee2..5191bcf322 100644 --- a/components/sceneutil/optimizer.hpp +++ b/components/sceneutil/optimizer.hpp @@ -285,9 +285,11 @@ class Optimizer BaseOptimizerVisitor(optimizer, FLATTEN_STATIC_TRANSFORMS) {} void apply(osg::Node& geode) override; + void apply(osg::Geometry& drawable) override; void apply(osg::Drawable& drawable) override; void apply(osg::Billboard& geode) override; - void apply(osg::Transform& transform) override; + void apply(osg::Transform& transform) override final; + void apply(osg::MatrixTransform& transform) override; bool removeTransforms(osg::Node* nodeWeCannotRemove); @@ -316,6 +318,7 @@ class Optimizer BaseOptimizerVisitor(optimizer, FLATTEN_STATIC_TRANSFORMS) {} void apply(osg::MatrixTransform& transform) override; + void apply(osg::Geometry&) override { } bool removeTransforms(osg::Node* nodeWeCannotRemove); @@ -338,6 +341,7 @@ class Optimizer BaseOptimizerVisitor(optimizer, REMOVE_REDUNDANT_NODES) {} void apply(osg::Group& group) override; + void apply(osg::Geometry&) override { } void removeEmptyNodes(); @@ -358,6 +362,7 @@ class Optimizer void apply(osg::Transform& transform) override; void apply(osg::LOD& lod) override; void apply(osg::Switch& switchNode) override; + void apply(osg::Geometry&) override { } bool isOperationPermissible(osg::Node& node); @@ -376,6 +381,7 @@ class Optimizer bool isOperationPermissible(osg::Group& node); + void apply(osg::Geometry&) override { } void apply(osg::Group& group) override; void apply(osg::LOD& lod) override; void apply(osg::Switch& switchNode) override; @@ -409,10 +415,10 @@ class Optimizer return _targetMaximumNumberOfVertices; } - void pushStateSet(osg::StateSet* stateSet); + bool pushStateSet(osg::StateSet* stateSet); void popStateSet(); void checkAlphaBlendingActive(); - + void apply(osg::Geometry&) override { } void apply(osg::Group& group) override; void apply(osg::Billboard&) override { /* don't do anything*/ } diff --git a/components/sceneutil/osgacontroller.hpp b/components/sceneutil/osgacontroller.hpp index 26212a3b99..893b8b1ebe 100644 --- a/components/sceneutil/osgacontroller.hpp +++ b/components/sceneutil/osgacontroller.hpp @@ -45,6 +45,8 @@ namespace SceneUtil META_Object(SceneUtil, OsgAnimationController) + osg::Callback* getAsCallback() override { return this; } + /// @brief Handles the location of the instance osg::Vec3f getTranslation(float time) const override; diff --git a/components/sceneutil/recastmesh.cpp b/components/sceneutil/recastmesh.cpp index 97efe010df..9614673a64 100644 --- a/components/sceneutil/recastmesh.cpp +++ b/components/sceneutil/recastmesh.cpp @@ -1,5 +1,6 @@ -#include "navmesh.hpp" +#include "recastmesh.hpp" #include "detourdebugdraw.hpp" +#include "depth.hpp" #include #include @@ -9,6 +10,7 @@ #include #include +#include #include #include @@ -45,7 +47,7 @@ namespace SceneUtil using namespace DetourNavigator; const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1.0f); + DebugDraw debugDraw(*group, DebugDraw::makeStateSet(), osg::Vec3f(0, 0, 0), 1.0f); const DetourNavigator::Mesh& mesh = recastMesh.getMesh(); std::vector indices = mesh.getIndices(); std::vector vertices = mesh.getVertices(); @@ -64,12 +66,19 @@ namespace SceneUtil const auto normals = calculateNormals(vertices, indices); const auto texScale = 1.0f / (settings.mCellSize * 10.0f); - duDebugDrawTriMesh(&debugDraw, vertices.data(), static_cast(vertices.size() / 3), - indices.data(), normals.data(), static_cast(indices.size() / 3), nullptr, texScale); + duDebugDrawTriMeshSlope(&debugDraw, vertices.data(), static_cast(vertices.size() / 3), + indices.data(), normals.data(), static_cast(indices.size() / 3), settings.mMaxSlope, texScale); osg::ref_ptr material = new osg::Material; material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); - group->getOrCreateStateSet()->setAttribute(material); + + const float polygonOffsetFactor = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + const float polygonOffsetUnits = SceneUtil::AutoDepth::isReversed() ? 1.0 : -1.0; + osg::ref_ptr polygonOffset = new osg::PolygonOffset(polygonOffsetFactor, polygonOffsetUnits); + + osg::ref_ptr stateSet = group->getOrCreateStateSet(); + stateSet->setAttribute(material); + stateSet->setAttributeAndModes(polygonOffset); return group; } diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index b9201fdf66..84b31f4afc 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "skeleton.hpp" #include "util.hpp" @@ -59,12 +61,22 @@ RigGeometry::RigGeometry(const RigGeometry ©, const osg::CopyOp ©op) void RigGeometry::setSourceGeometry(osg::ref_ptr sourceGeometry) { + for (unsigned int i=0; i<2; ++i) + mGeometry[i] = nullptr; + mSourceGeometry = sourceGeometry; for (unsigned int i=0; i<2; ++i) { const osg::Geometry& from = *sourceGeometry; + + // DO NOT COPY AND PASTE THIS CODE. Cloning osg::Geometry without also cloning its contained Arrays is generally unsafe. + // In this specific case the operation is safe under the following two assumptions: + // - When Arrays are removed or replaced in the cloned geometry, the original Arrays in their place must outlive the cloned geometry regardless. (ensured by mSourceGeometry) + // - Arrays that we add or replace in the cloned geometry must be explicitely forbidden from reusing BufferObjects of the original geometry. (ensured by vbo below) mGeometry[i] = new osg::Geometry(from, osg::CopyOp::SHALLOW_COPY); + mGeometry[i]->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(mSourceGeometry)); + osg::Geometry& to = *mGeometry[i]; to.setSupportsDisplayList(false); to.setUseVertexBufferObjects(true); @@ -114,9 +126,11 @@ osg::ref_ptr RigGeometry::getSourceGeometry() const bool RigGeometry::initFromParentSkeleton(osg::NodeVisitor* nv) { const osg::NodePath& path = nv->getNodePath(); - for (osg::NodePath::const_reverse_iterator it = path.rbegin(); it != path.rend(); ++it) + for (osg::NodePath::const_reverse_iterator it = path.rbegin()+1; it != path.rend(); ++it) { osg::Node* node = *it; + if (node->asTransform()) + continue; if (Skeleton* skel = dynamic_cast(node)) { mSkeleton = skel; @@ -246,8 +260,8 @@ void RigGeometry::cull(osg::NodeVisitor* nv) if (tangentDst) tangentDst->dirty(); -#if OSG_MIN_VERSION_REQUIRED(3, 5, 6) - geom.dirtyGLObjects(); +#if OSG_MIN_VERSION_REQUIRED(3, 5, 10) + geom.osg::Drawable::dirtyGLObjects(); #endif nv->pushOntoNodePath(&geom); @@ -310,8 +324,10 @@ void RigGeometry::updateBounds(osg::NodeVisitor *nv) void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath) { bool foundSkel = false; - osg::ref_ptr geomToSkelMatrix; - for (osg::NodePath::const_iterator it = nodePath.begin(); it != nodePath.end(); ++it) + osg::RefMatrix* geomToSkelMatrix = mGeomToSkelMatrix; + if (geomToSkelMatrix) + geomToSkelMatrix->makeIdentity(); + for (osg::NodePath::const_iterator it = nodePath.begin(); it != nodePath.end()-1; ++it) { osg::Node* node = *it; if (!foundSkel) @@ -323,14 +339,15 @@ void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath) { if (osg::Transform* trans = node->asTransform()) { + osg::MatrixTransform* matrixTrans = trans->asMatrixTransform(); + if (matrixTrans && matrixTrans->getMatrix().isIdentity()) + continue; if (!geomToSkelMatrix) - geomToSkelMatrix = new osg::RefMatrix; + geomToSkelMatrix = mGeomToSkelMatrix = new osg::RefMatrix; trans->computeWorldToLocalMatrix(*geomToSkelMatrix, nullptr); } } } - if (geomToSkelMatrix && !geomToSkelMatrix->isIdentity()) - mGeomToSkelMatrix = geomToSkelMatrix; } void RigGeometry::setInfluenceMap(osg::ref_ptr influenceMap) diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index e01583399e..25ae5a3243 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -9,6 +9,13 @@ namespace SceneUtil class Skeleton; class Bone; + // TODO: This class has a lot of issues. + // - We require too many workarounds to ensure safety. + // - mSourceGeometry should be const, but can not be const because of a use case in shadervisitor.cpp. + // - We create useless mGeometry clones in template RigGeometries. + // - We do not support compileGLObjects. + // - We duplicate some code in MorphGeometry. + /// @brief Mesh skinning implementation. /// @note A RigGeometry may be attached directly to a Skeleton, or somewhere below a Skeleton. /// Note though that the RigGeometry ignores any transforms below the Skeleton, so the attachment point is not that important. diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp index 3b60c80afd..0765a2e835 100644 --- a/components/sceneutil/rtt.cpp +++ b/components/sceneutil/rtt.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace SceneUtil { @@ -69,6 +70,7 @@ namespace SceneUtil camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); camera->setViewport(0, 0, mTextureWidth, mTextureHeight); + SceneUtil::setCameraClearDepth(camera); setDefaults(mViewDependentDataMap[cv]->mCamera.get()); diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 703b63af7d..964b8bc4c2 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -123,17 +123,18 @@ void registerSerializers() "Resource::TemplateRef", "Resource::TemplateMultiRef", "SceneUtil::CompositeStateSetUpdater", + "SceneUtil::UBOManager", "SceneUtil::LightListCallback", "SceneUtil::LightManagerUpdateCallback", + "SceneUtil::FFPLightStateAttribute", "SceneUtil::UpdateRigBounds", "SceneUtil::UpdateRigGeometry", "SceneUtil::LightSource", - "SceneUtil::StateSetUpdater", "SceneUtil::DisableLight", "SceneUtil::MWShadowTechnique", "SceneUtil::TextKeyMapHolder", + "Shader::AddedState", "Shader::RemovedAlphaFunc", - "NifOsg::LightManagerStateAttribute", "NifOsg::FlipController", "NifOsg::KeyframeController", "NifOsg::Emitter", diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 3244dc672a..dfe4cf1507 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -1,10 +1,13 @@ #include "shadow.hpp" #include +#include #include #include +#include "mwshadowtechnique.hpp" + namespace SceneUtil { using namespace osgShadow; @@ -24,8 +27,7 @@ namespace SceneUtil mShadowSettings->setLightNum(0); mShadowSettings->setReceivesShadowTraversalMask(~0u); - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); mShadowSettings->setNumShadowMapsPerLight(numberOfShadowMapsPerLight); mShadowSettings->setBaseShadowTextureUnit(8 - numberOfShadowMapsPerLight); @@ -33,7 +35,7 @@ namespace SceneUtil const float maximumShadowMapDistance = Settings::Manager::getFloat("maximum shadow map distance", "Shadows"); if (maximumShadowMapDistance > 0) { - const float shadowFadeStart = std::min(std::max(0.f, Settings::Manager::getFloat("shadow fade start", "Shadows")), 1.f); + const float shadowFadeStart = std::clamp(Settings::Manager::getFloat("shadow fade start", "Shadows"), 0.f, 1.f); mShadowSettings->setMaximumShadowMapDistance(maximumShadowMapDistance); mShadowTechnique->setShadowFadeStart(maximumShadowMapDistance * shadowFadeStart); } @@ -75,8 +77,7 @@ namespace SceneUtil if (!Settings::Manager::getBool("enable shadows", "Shadows")) return; - int numberOfShadowMapsPerLight = Settings::Manager::getInt("number of shadow maps", "Shadows"); - numberOfShadowMapsPerLight = std::max(1, std::min(numberOfShadowMapsPerLight, 8)); + const int numberOfShadowMapsPerLight = std::clamp(Settings::Manager::getInt("number of shadow maps", "Shadows"), 1, 8); int baseShadowTextureUnit = 8 - numberOfShadowMapsPerLight; @@ -94,7 +95,7 @@ namespace SceneUtil } } - ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager) : mShadowedScene(new osgShadow::ShadowedScene), + ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, Shader::ShaderManager &shaderManager) : mShadowedScene(new osgShadow::ShadowedScene), mShadowTechnique(new MWShadowTechnique), mOutdoorShadowCastingMask(outdoorShadowCastingMask), mIndoorShadowCastingMask(indoorShadowCastingMask) @@ -109,10 +110,15 @@ namespace SceneUtil setupShadowSettings(); mShadowTechnique->setupCastingShader(shaderManager); + mShadowTechnique->setWorldMask(worldMask); enableOutdoorMode(); } + ShadowManager::~ShadowManager() + { + } + Shader::ShaderManager::DefineMap ShadowManager::getShadowDefines() { if (!mEnableShadows) diff --git a/components/sceneutil/shadow.hpp b/components/sceneutil/shadow.hpp index c823ecf860..4c34944f4a 100644 --- a/components/sceneutil/shadow.hpp +++ b/components/sceneutil/shadow.hpp @@ -1,15 +1,22 @@ #ifndef COMPONENTS_SCENEUTIL_SHADOW_H #define COMPONENTS_SCENEUTIL_SHADOW_H -#include -#include - #include -#include "mwshadowtechnique.hpp" +namespace osg +{ + class StateSet; + class Group; +} +namespace osgShadow +{ + class ShadowSettings; + class ShadowedScene; +} namespace SceneUtil { + class MWShadowTechnique; class ShadowManager { public: @@ -17,7 +24,8 @@ namespace SceneUtil static Shader::ShaderManager::DefineMap getShadowsDisabledDefines(); - ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager); + ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, unsigned int worldMask, Shader::ShaderManager &shaderManager); + ~ShadowManager(); void setupShadowSettings(); diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp index af62b581c9..3e933cbb98 100644 --- a/components/sceneutil/shadowsbin.cpp +++ b/components/sceneutil/shadowsbin.cpp @@ -42,11 +42,7 @@ namespace namespace SceneUtil { -std::array, GL_ALWAYS - GL_NEVER + 1> ShadowsBin::sCastingPrograms = { - nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr -}; - -ShadowsBin::ShadowsBin() +ShadowsBin::ShadowsBin(const CastingPrograms& castingPrograms) { mNoTestStateSet = new osg::StateSet; mNoTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); @@ -57,10 +53,10 @@ ShadowsBin::ShadowsBin() mShaderAlphaTestStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); mShaderAlphaTestStateSet->setMode(GL_BLEND, osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); - for (size_t i = 0; i < sCastingPrograms.size(); ++i) + for (size_t i = 0; i < castingPrograms.size(); ++i) { mAlphaFuncShaders[i] = new osg::StateSet; - mAlphaFuncShaders[i]->setAttribute(sCastingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); + mAlphaFuncShaders[i]->setAttribute(castingPrograms[i], osg::StateAttribute::ON | osg::StateAttribute::PROTECTED | osg::StateAttribute::OVERRIDE); } } @@ -150,13 +146,6 @@ StateGraph* ShadowsBin::cullStateGraph(StateGraph* sg, StateGraph* root, std::un return sg; } -void ShadowsBin::addPrototype(const std::string & name, const std::array, GL_ALWAYS - GL_NEVER + 1>& castingPrograms) -{ - sCastingPrograms = castingPrograms; - osg::ref_ptr bin(new ShadowsBin); - osgUtil::RenderBin::addRenderBinPrototype(name, bin); -} - inline bool ShadowsBin::State::needTexture() const { return mAlphaBlend || (mAlphaFunc && mAlphaFunc->getFunction() != GL_ALWAYS); diff --git a/components/sceneutil/shadowsbin.hpp b/components/sceneutil/shadowsbin.hpp index 1c63caf4bb..2c838d509e 100644 --- a/components/sceneutil/shadowsbin.hpp +++ b/components/sceneutil/shadowsbin.hpp @@ -12,20 +12,17 @@ namespace osg namespace SceneUtil { - /// renderbin which culls redundant state for shadow map rendering class ShadowsBin : public osgUtil::RenderBin { - private: - static std::array, GL_ALWAYS - GL_NEVER + 1> sCastingPrograms; + public: + template + using Array = std::array; - osg::ref_ptr mNoTestStateSet; - osg::ref_ptr mShaderAlphaTestStateSet; + using CastingPrograms = Array>; - std::array, GL_ALWAYS - GL_NEVER + 1> mAlphaFuncShaders; - public: META_Object(SceneUtil, ShadowsBin) - ShadowsBin(); + ShadowsBin(const CastingPrograms& castingPrograms); ShadowsBin(const ShadowsBin& rhs, const osg::CopyOp& copyop) : osgUtil::RenderBin(rhs, copyop) , mNoTestStateSet(rhs.mNoTestStateSet) @@ -65,15 +62,14 @@ namespace SceneUtil osgUtil::StateGraph* cullStateGraph(osgUtil::StateGraph* sg, osgUtil::StateGraph* root, std::unordered_set& uninteresting, bool cullFaceOverridden); - static void addPrototype(const std::string& name, const std::array, GL_ALWAYS - GL_NEVER + 1>& castingPrograms); - }; + private: + ShadowsBin() {} - class ShadowsBinAdder - { - public: - ShadowsBinAdder(const std::string& name, const std::array, GL_ALWAYS - GL_NEVER + 1>& castingPrograms){ ShadowsBin::addPrototype(name, castingPrograms); } - }; + osg::ref_ptr mNoTestStateSet; + osg::ref_ptr mShaderAlphaTestStateSet; + Array> mAlphaFuncShaders; + }; } #endif diff --git a/components/sceneutil/skeleton.cpp b/components/sceneutil/skeleton.cpp index 40f524e0a2..c465333fb4 100644 --- a/components/sceneutil/skeleton.cpp +++ b/components/sceneutil/skeleton.cpp @@ -1,6 +1,5 @@ #include "skeleton.hpp" -#include #include #include @@ -12,24 +11,24 @@ namespace SceneUtil class InitBoneCacheVisitor : public osg::NodeVisitor { public: - InitBoneCacheVisitor(std::map >& cache) + typedef std::vector TransformPath; + InitBoneCacheVisitor(std::unordered_map& cache) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCache(cache) { } - void apply(osg::Transform &node) override + void apply(osg::MatrixTransform &node) override { - osg::MatrixTransform* bone = node.asMatrixTransform(); - if (!bone) - return; - - mCache[Misc::StringUtils::lowerCase(bone->getName())] = std::make_pair(getNodePath(), bone); - + mPath.push_back(&node); + mCache[Misc::StringUtils::lowerCase(node.getName())] = mPath; traverse(node); + mPath.pop_back(); } + private: - std::map >& mCache; + TransformPath mPath; + std::unordered_map& mCache; }; Skeleton::Skeleton() @@ -73,18 +72,13 @@ Bone* Skeleton::getBone(const std::string &name) mRootBone.reset(new Bone); } - const osg::NodePath& path = found->second.first; Bone* bone = mRootBone.get(); - for (osg::NodePath::const_iterator it = path.begin(); it != path.end(); ++it) + for (osg::MatrixTransform* matrixTransform : found->second) { - osg::MatrixTransform* matrixTransform = dynamic_cast(*it); - if (!matrixTransform) - continue; - Bone* child = nullptr; for (unsigned int i=0; imChildren.size(); ++i) { - if (bone->mChildren[i]->mNode == *it) + if (bone->mChildren[i]->mNode == matrixTransform) { child = bone->mChildren[i]; break; diff --git a/components/sceneutil/skeleton.hpp b/components/sceneutil/skeleton.hpp index 22988dfd5f..10f4640249 100644 --- a/components/sceneutil/skeleton.hpp +++ b/components/sceneutil/skeleton.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace SceneUtil { @@ -72,7 +73,7 @@ namespace SceneUtil // As far as the scene graph goes we support multiple root bones. std::unique_ptr mRootBone; - typedef std::map > BoneCache; + typedef std::unordered_map > BoneCache; BoneCache mBoneCache; bool mBoneCacheInit; diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 35be9cb434..cc2e248457 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -34,8 +34,6 @@ namespace SceneUtil StateSetUpdater(); StateSetUpdater(const StateSetUpdater& copy, const osg::CopyOp& copyop); - META_Object(SceneUtil, StateSetUpdater) - void operator()(osg::Node* node, osg::NodeVisitor* nv); /// Apply state - to override in derived classes diff --git a/components/sceneutil/unrefqueue.cpp b/components/sceneutil/unrefqueue.cpp deleted file mode 100644 index 50b23cae92..0000000000 --- a/components/sceneutil/unrefqueue.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "unrefqueue.hpp" - -//#include - -//#include - -namespace SceneUtil -{ - void UnrefWorkItem::doWork() - { - mObjects.clear(); - } - - UnrefQueue::UnrefQueue() - { - mWorkItem = new UnrefWorkItem; - } - - void UnrefQueue::push(const osg::Referenced *obj) - { - mWorkItem->mObjects.emplace_back(obj); - } - - void UnrefQueue::flush(SceneUtil::WorkQueue *workQueue) - { - if (mWorkItem->mObjects.empty()) - return; - - workQueue->addWorkItem(mWorkItem, true); - - mWorkItem = new UnrefWorkItem; - } - - unsigned int UnrefQueue::getNumItems() const - { - return mWorkItem->mObjects.size(); - } - -} diff --git a/components/sceneutil/unrefqueue.hpp b/components/sceneutil/unrefqueue.hpp deleted file mode 100644 index 84372d28c0..0000000000 --- a/components/sceneutil/unrefqueue.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef OPENMW_COMPONENTS_UNREFQUEUE_H -#define OPENMW_COMPONENTS_UNREFQUEUE_H - -#include - -#include -#include - -#include - -namespace SceneUtil -{ - class WorkQueue; - - class UnrefWorkItem : public SceneUtil::WorkItem - { - public: - std::deque > mObjects; - void doWork() override; - }; - - /// @brief Handles unreferencing of objects through the WorkQueue. Typical use scenario - /// would be the main thread pushing objects that are no longer needed, and the background thread deleting them. - class UnrefQueue : public osg::Referenced - { - public: - UnrefQueue(); - - /// Adds an object to the list of objects to be unreferenced. Call from the main thread. - void push(const osg::Referenced* obj); - - /// Adds a WorkItem to the given WorkQueue that will clear the list of objects in a worker thread, thus unreferencing them. - /// Call from the main thread. - void flush(SceneUtil::WorkQueue* workQueue); - - unsigned int getNumItems() const; - - private: - osg::ref_ptr mWorkItem; - }; - -} - -#endif diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index 5c2f8c2934..7065fae933 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -4,40 +4,18 @@ #include #include -#include - #include #include #include #include -#include #include #include #include #include #include -#include -#include #include -#ifndef GL_DEPTH32F_STENCIL8_NV -#define GL_DEPTH32F_STENCIL8_NV 0x8DAC -#endif - -namespace -{ - -bool isReverseZSupported() -{ - if (!Settings::Manager::mDefaultSettings.count({"Camera", "reverse z"})) - return false; - auto ext = osg::GLExtensions::Get(0, false); - return Settings::Manager::getBool("reverse z", "Camera") && ext && ext->isClipControlSupported; -} - -} - namespace SceneUtil { @@ -324,12 +302,6 @@ 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; @@ -345,81 +317,4 @@ bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg:: return addMSAAIntermediateTarget; } -void attachAlphaToCoverageFriendlyDepthColor(osg::Camera* camera, osg::Texture2D* colorTex, osg::Texture2D* depthTex, GLenum depthFormat) -{ - bool addMSAAIntermediateTarget = Settings::Manager::getBool("antialias alpha test", "Shaders") && Settings::Manager::getInt("antialiasing", "Video") > 1; - - if (isFloatingPointDepthFormat(depthFormat) && addMSAAIntermediateTarget) - { - camera->attach(osg::Camera::COLOR_BUFFER0, colorTex); - camera->attach(osg::Camera::DEPTH_BUFFER, depthTex); - camera->addCullCallback(new AttachMultisampledDepthColorCallback(colorTex, depthTex, 2, 1)); - } - else - { - attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, colorTex); - camera->attach(osg::Camera::DEPTH_BUFFER, depthTex); - } -} - -bool getReverseZ() -{ - static bool reverseZ = isReverseZSupported(); - return reverseZ; -} - -void setCameraClearDepth(osg::Camera* camera) -{ - camera->setClearDepth(getReverseZ() ? 0.0 : 1.0); -} - -osg::ref_ptr createDepth() -{ - return new osg::Depth(getReverseZ() ? osg::Depth::GEQUAL : osg::Depth::LEQUAL); -} - -osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near) -{ - double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); - return osg::Matrix( - A/aspect, 0, 0, 0, - 0, A, 0, 0, - 0, 0, 0, -1, - 0, 0, near, 0 - ); -} - -osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far) -{ - double A = 1.0/std::tan(osg::DegreesToRadians(fov)/2.0); - return osg::Matrix( - A/aspect, 0, 0, 0, - 0, A, 0, 0, - 0, 0, near/(far-near), -1, - 0, 0, (far*near)/(far - near), 0 - ); -} - -osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far) -{ - return osg::Matrix( - 2/(right-left), 0, 0, 0, - 0, 2/(top-bottom), 0, 0, - 0, 0, 1/(far-near), 0, - (right+left)/(left-right), (top+bottom)/(bottom-top), far/(far-near), 1 - ); -} - -bool isFloatingPointDepthFormat(GLenum format) -{ - constexpr std::array formats = { - GL_DEPTH_COMPONENT32F, - GL_DEPTH_COMPONENT32F_NV, - GL_DEPTH32F_STENCIL8, - GL_DEPTH32F_STENCIL8_NV, - }; - - return std::find(formats.cbegin(), formats.cend(), format) != formats.cend(); -} - } diff --git a/components/sceneutil/util.hpp b/components/sceneutil/util.hpp index 4d19714d66..ddc2db845d 100644 --- a/components/sceneutil/util.hpp +++ b/components/sceneutil/util.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include @@ -64,31 +63,6 @@ namespace SceneUtil // Alpha-to-coverage requires a multisampled framebuffer, so we need to set that up for RTTs bool attachAlphaToCoverageFriendlyFramebufferToCamera(osg::Camera* camera, osg::Camera::BufferComponent buffer, osg::Texture* texture, unsigned int level = 0, unsigned int face = 0, bool mipMapGeneration = false); - - void attachAlphaToCoverageFriendlyDepthColor(osg::Camera* camera, osg::Texture2D* colorTex, osg::Texture2D* depthTex, GLenum depthFormat); - - bool getReverseZ(); - - void setCameraClearDepth(osg::Camera* camera); - - // Returns a suitable depth state attribute dependent on whether a reverse-z - // depth buffer is in use. - osg::ref_ptr createDepth(); - - // Returns a perspective projection matrix for use with a reversed z-buffer - // and an infinite far plane. This is derived by mapping the default z-range - // of [0,1] to [1,0], then taking the limit as far plane approaches - // infinity. - osg::Matrix getReversedZProjectionMatrixAsPerspectiveInf(double fov, double aspect, double near); - - // Returns a perspective projection matrix for use with a reversed z-buffer. - osg::Matrix getReversedZProjectionMatrixAsPerspective(double fov, double aspect, double near, double far); - - // Returns an orthographic projection matrix for use with a reversed z-buffer. - osg::Matrix getReversedZProjectionMatrixAsOrtho(double left, double right, double bottom, double top, double near, double far); - - // Returns true if the GL format is a floating point depth format - bool isFloatingPointDepthFormat(GLenum format); } #endif diff --git a/components/sceneutil/visitor.cpp b/components/sceneutil/visitor.cpp index fde87c66ba..ea09445678 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -51,18 +51,17 @@ namespace SceneUtil void NodeMapVisitor::apply(osg::MatrixTransform& trans) { - // Take transformation for first found node in file - std::string originalNodeName = Misc::StringUtils::lowerCase(trans.getName()); + // Choose first found node in file if (trans.libraryName() == std::string("osgAnimation")) { + std::string nodeName = trans.getName(); // Convert underscores to whitespaces as a workaround for Collada (OpenMW's animation system uses whitespace-separated names) - std::replace(originalNodeName.begin(), originalNodeName.end(), '_', ' '); + std::replace(nodeName.begin(), nodeName.end(), '_', ' '); + mMap.emplace(nodeName, &trans); } - - const std::string nodeName = originalNodeName; - - mMap.emplace(nodeName, &trans); + else + mMap.emplace(trans.getName(), &trans); traverse(trans); } diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index fcf3c1f944..45aa408b9e 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -4,6 +4,10 @@ #include #include +#include + +#include + // Commonly used scene graph visitors namespace SceneUtil { @@ -49,7 +53,7 @@ namespace SceneUtil class NodeMapVisitor : public osg::NodeVisitor { public: - typedef std::map > NodeMap; + typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; NodeMapVisitor(NodeMap& map) : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) diff --git a/components/sceneutil/waterutil.cpp b/components/sceneutil/waterutil.cpp index ac171005ae..e22070f40f 100644 --- a/components/sceneutil/waterutil.cpp +++ b/components/sceneutil/waterutil.cpp @@ -5,7 +5,7 @@ #include #include -#include "util.hpp" +#include "depth.hpp" namespace SceneUtil { @@ -78,7 +78,7 @@ namespace SceneUtil stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - auto depth = createDepth(); + osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); diff --git a/components/sdlutil/sdlmappings.cpp b/components/sdlutil/sdlmappings.cpp new file mode 100644 index 0000000000..8306909ee5 --- /dev/null +++ b/components/sdlutil/sdlmappings.cpp @@ -0,0 +1,251 @@ +#include "sdlmappings.hpp" + +#include + +#include + +#include +#include + +namespace SDLUtil +{ + std::string sdlControllerButtonToString(int button) + { + switch(button) + { + case SDL_CONTROLLER_BUTTON_A: + return "A Button"; + case SDL_CONTROLLER_BUTTON_B: + return "B Button"; + case SDL_CONTROLLER_BUTTON_BACK: + return "Back Button"; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return "DPad Down"; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return "DPad Left"; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return "DPad Right"; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + return "DPad Up"; + case SDL_CONTROLLER_BUTTON_GUIDE: + return "Guide Button"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return "Left Shoulder"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return "Left Stick Button"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return "Right Shoulder"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return "Right Stick Button"; + case SDL_CONTROLLER_BUTTON_START: + return "Start Button"; + case SDL_CONTROLLER_BUTTON_X: + return "X Button"; + case SDL_CONTROLLER_BUTTON_Y: + return "Y Button"; + default: + return "Button " + std::to_string(button); + } + } + + std::string sdlControllerAxisToString(int axis) + { + switch(axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + return "Left Stick X"; + case SDL_CONTROLLER_AXIS_LEFTY: + return "Left Stick Y"; + case SDL_CONTROLLER_AXIS_RIGHTX: + return "Right Stick X"; + case SDL_CONTROLLER_AXIS_RIGHTY: + return "Right Stick Y"; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return "Left Trigger"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return "Right Trigger"; + default: + return "Axis " + std::to_string(axis); + } + } + + MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button) + { + //The right button is the second button, according to MyGUI + if(button == SDL_BUTTON_RIGHT) + button = SDL_BUTTON_MIDDLE; + else if(button == SDL_BUTTON_MIDDLE) + button = SDL_BUTTON_RIGHT; + + //MyGUI's buttons are 0 indexed + return MyGUI::MouseButton::Enum(button - 1); + } + + Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button) + { + Uint8 value = button.getValue() + 1; + if (value == SDL_BUTTON_RIGHT) + value = SDL_BUTTON_MIDDLE; + else if (value == SDL_BUTTON_MIDDLE) + value = SDL_BUTTON_RIGHT; + return value; + } + + namespace + { + std::map initKeyMap() + { + std::map keyMap; + keyMap[SDLK_UNKNOWN] = MyGUI::KeyCode::None; + keyMap[SDLK_ESCAPE] = MyGUI::KeyCode::Escape; + keyMap[SDLK_1] = MyGUI::KeyCode::One; + keyMap[SDLK_2] = MyGUI::KeyCode::Two; + keyMap[SDLK_3] = MyGUI::KeyCode::Three; + keyMap[SDLK_4] = MyGUI::KeyCode::Four; + keyMap[SDLK_5] = MyGUI::KeyCode::Five; + keyMap[SDLK_6] = MyGUI::KeyCode::Six; + keyMap[SDLK_7] = MyGUI::KeyCode::Seven; + keyMap[SDLK_8] = MyGUI::KeyCode::Eight; + keyMap[SDLK_9] = MyGUI::KeyCode::Nine; + keyMap[SDLK_0] = MyGUI::KeyCode::Zero; + keyMap[SDLK_MINUS] = MyGUI::KeyCode::Minus; + keyMap[SDLK_EQUALS] = MyGUI::KeyCode::Equals; + keyMap[SDLK_BACKSPACE] = MyGUI::KeyCode::Backspace; + keyMap[SDLK_TAB] = MyGUI::KeyCode::Tab; + keyMap[SDLK_q] = MyGUI::KeyCode::Q; + keyMap[SDLK_w] = MyGUI::KeyCode::W; + keyMap[SDLK_e] = MyGUI::KeyCode::E; + keyMap[SDLK_r] = MyGUI::KeyCode::R; + keyMap[SDLK_t] = MyGUI::KeyCode::T; + keyMap[SDLK_y] = MyGUI::KeyCode::Y; + keyMap[SDLK_u] = MyGUI::KeyCode::U; + keyMap[SDLK_i] = MyGUI::KeyCode::I; + keyMap[SDLK_o] = MyGUI::KeyCode::O; + keyMap[SDLK_p] = MyGUI::KeyCode::P; + keyMap[SDLK_RETURN] = MyGUI::KeyCode::Return; + keyMap[SDLK_a] = MyGUI::KeyCode::A; + keyMap[SDLK_s] = MyGUI::KeyCode::S; + keyMap[SDLK_d] = MyGUI::KeyCode::D; + keyMap[SDLK_f] = MyGUI::KeyCode::F; + keyMap[SDLK_g] = MyGUI::KeyCode::G; + keyMap[SDLK_h] = MyGUI::KeyCode::H; + keyMap[SDLK_j] = MyGUI::KeyCode::J; + keyMap[SDLK_k] = MyGUI::KeyCode::K; + keyMap[SDLK_l] = MyGUI::KeyCode::L; + keyMap[SDLK_SEMICOLON] = MyGUI::KeyCode::Semicolon; + keyMap[SDLK_QUOTE] = MyGUI::KeyCode::Apostrophe; + keyMap[SDLK_BACKQUOTE] = MyGUI::KeyCode::Grave; + keyMap[SDLK_LSHIFT] = MyGUI::KeyCode::LeftShift; + keyMap[SDLK_BACKSLASH] = MyGUI::KeyCode::Backslash; + keyMap[SDLK_z] = MyGUI::KeyCode::Z; + keyMap[SDLK_x] = MyGUI::KeyCode::X; + keyMap[SDLK_c] = MyGUI::KeyCode::C; + keyMap[SDLK_v] = MyGUI::KeyCode::V; + keyMap[SDLK_b] = MyGUI::KeyCode::B; + keyMap[SDLK_n] = MyGUI::KeyCode::N; + keyMap[SDLK_m] = MyGUI::KeyCode::M; + keyMap[SDLK_COMMA] = MyGUI::KeyCode::Comma; + keyMap[SDLK_PERIOD] = MyGUI::KeyCode::Period; + keyMap[SDLK_SLASH] = MyGUI::KeyCode::Slash; + keyMap[SDLK_RSHIFT] = MyGUI::KeyCode::RightShift; + keyMap[SDLK_KP_MULTIPLY] = MyGUI::KeyCode::Multiply; + keyMap[SDLK_LALT] = MyGUI::KeyCode::LeftAlt; + keyMap[SDLK_SPACE] = MyGUI::KeyCode::Space; + keyMap[SDLK_CAPSLOCK] = MyGUI::KeyCode::Capital; + keyMap[SDLK_F1] = MyGUI::KeyCode::F1; + keyMap[SDLK_F2] = MyGUI::KeyCode::F2; + keyMap[SDLK_F3] = MyGUI::KeyCode::F3; + keyMap[SDLK_F4] = MyGUI::KeyCode::F4; + keyMap[SDLK_F5] = MyGUI::KeyCode::F5; + keyMap[SDLK_F6] = MyGUI::KeyCode::F6; + keyMap[SDLK_F7] = MyGUI::KeyCode::F7; + keyMap[SDLK_F8] = MyGUI::KeyCode::F8; + keyMap[SDLK_F9] = MyGUI::KeyCode::F9; + keyMap[SDLK_F10] = MyGUI::KeyCode::F10; + keyMap[SDLK_NUMLOCKCLEAR] = MyGUI::KeyCode::NumLock; + keyMap[SDLK_SCROLLLOCK] = MyGUI::KeyCode::ScrollLock; + keyMap[SDLK_KP_7] = MyGUI::KeyCode::Numpad7; + keyMap[SDLK_KP_8] = MyGUI::KeyCode::Numpad8; + keyMap[SDLK_KP_9] = MyGUI::KeyCode::Numpad9; + keyMap[SDLK_KP_MINUS] = MyGUI::KeyCode::Subtract; + keyMap[SDLK_KP_4] = MyGUI::KeyCode::Numpad4; + keyMap[SDLK_KP_5] = MyGUI::KeyCode::Numpad5; + keyMap[SDLK_KP_6] = MyGUI::KeyCode::Numpad6; + keyMap[SDLK_KP_PLUS] = MyGUI::KeyCode::Add; + keyMap[SDLK_KP_1] = MyGUI::KeyCode::Numpad1; + keyMap[SDLK_KP_2] = MyGUI::KeyCode::Numpad2; + keyMap[SDLK_KP_3] = MyGUI::KeyCode::Numpad3; + keyMap[SDLK_KP_0] = MyGUI::KeyCode::Numpad0; + keyMap[SDLK_KP_PERIOD] = MyGUI::KeyCode::Decimal; + keyMap[SDLK_F11] = MyGUI::KeyCode::F11; + keyMap[SDLK_F12] = MyGUI::KeyCode::F12; + keyMap[SDLK_F13] = MyGUI::KeyCode::F13; + keyMap[SDLK_F14] = MyGUI::KeyCode::F14; + keyMap[SDLK_F15] = MyGUI::KeyCode::F15; + keyMap[SDLK_KP_EQUALS] = MyGUI::KeyCode::NumpadEquals; + keyMap[SDLK_COLON] = MyGUI::KeyCode::Colon; + keyMap[SDLK_KP_ENTER] = MyGUI::KeyCode::NumpadEnter; + keyMap[SDLK_KP_DIVIDE] = MyGUI::KeyCode::Divide; + keyMap[SDLK_SYSREQ] = MyGUI::KeyCode::SysRq; + keyMap[SDLK_RALT] = MyGUI::KeyCode::RightAlt; + keyMap[SDLK_HOME] = MyGUI::KeyCode::Home; + keyMap[SDLK_UP] = MyGUI::KeyCode::ArrowUp; + keyMap[SDLK_PAGEUP] = MyGUI::KeyCode::PageUp; + keyMap[SDLK_LEFT] = MyGUI::KeyCode::ArrowLeft; + keyMap[SDLK_RIGHT] = MyGUI::KeyCode::ArrowRight; + keyMap[SDLK_END] = MyGUI::KeyCode::End; + keyMap[SDLK_DOWN] = MyGUI::KeyCode::ArrowDown; + keyMap[SDLK_PAGEDOWN] = MyGUI::KeyCode::PageDown; + keyMap[SDLK_INSERT] = MyGUI::KeyCode::Insert; + keyMap[SDLK_DELETE] = MyGUI::KeyCode::Delete; + keyMap[SDLK_APPLICATION] = MyGUI::KeyCode::AppMenu; + + //The function of the Ctrl and Meta keys are switched on macOS compared to other platforms. + //For instance] = Cmd+C versus Ctrl+C to copy from the system clipboard + #if defined(__APPLE__) + keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftControl; + keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightControl; + keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftWindows; + keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightWindows; + #else + keyMap[SDLK_LGUI] = MyGUI::KeyCode::LeftWindows; + keyMap[SDLK_RGUI] = MyGUI::KeyCode::RightWindows; + keyMap[SDLK_LCTRL] = MyGUI::KeyCode::LeftControl; + keyMap[SDLK_RCTRL] = MyGUI::KeyCode::RightControl; + #endif + return keyMap; + } + + std::map reverseKeyMap(const std::map& map) + { + std::map result; + for (auto [sdl, mygui] : map) + result[mygui] = sdl; + return result; + } + } + + MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code) + { + static std::map keyMap = initKeyMap(); + + MyGUI::KeyCode kc = MyGUI::KeyCode::None; + auto foundKey = keyMap.find(code); + if (foundKey != keyMap.end()) + kc = foundKey->second; + + return kc; + } + + SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button) + { + static auto keyMap = reverseKeyMap(initKeyMap()); + + SDL_Keycode kc = 0; + auto foundKey = keyMap.find(button); + if (foundKey != keyMap.end()) + kc = foundKey->second; + + return kc; + } +} diff --git a/apps/openmw/mwinput/sdlmappings.hpp b/components/sdlutil/sdlmappings.hpp similarity index 50% rename from apps/openmw/mwinput/sdlmappings.hpp rename to components/sdlutil/sdlmappings.hpp index 0cdd4694f5..3625075009 100644 --- a/apps/openmw/mwinput/sdlmappings.hpp +++ b/components/sdlutil/sdlmappings.hpp @@ -1,5 +1,5 @@ -#ifndef MWINPUT_SDLMAPPINGS_H -#define MWINPUT_SDLMAPPINGS_H +#ifndef SDLUTIL_SDLMAPPINGS +#define SDLUTIL_SDLMAPPINGS #include @@ -12,14 +12,16 @@ namespace MyGUI struct MouseButton; } -namespace MWInput +namespace SDLUtil { std::string sdlControllerButtonToString(int button); std::string sdlControllerAxisToString(int axis); - MyGUI::MouseButton sdlButtonToMyGUI(Uint8 button); + MyGUI::MouseButton sdlMouseButtonToMyGui(Uint8 button); + Uint8 myGuiMouseButtonToSdl(MyGUI::MouseButton button); MyGUI::KeyCode sdlKeyToMyGUI(SDL_Keycode code); + SDL_Keycode myGuiKeyToSdl(MyGUI::KeyCode button); } -#endif +#endif // !SDLUTIL_SDLMAPPINGS diff --git a/components/settings/categories.hpp b/components/settings/categories.hpp index d6cd042f61..6a2da2fa10 100644 --- a/components/settings/categories.hpp +++ b/components/settings/categories.hpp @@ -9,7 +9,7 @@ namespace Settings { using CategorySetting = std::pair; - using CategorySettingVector = std::set>; + using CategorySettingVector = std::set; using CategorySettingValueMap = std::map; } diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 09a3d1f516..cef627acd4 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -39,7 +39,7 @@ void Manager::saveUser(const std::string &file) std::string Manager::getString(const std::string &setting, const std::string &category) { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::key_type key (category, setting); CategorySettingValueMap::iterator it = mUserSettings.find(key); if (it != mUserSettings.end()) return it->second; @@ -93,7 +93,7 @@ osg::Vec2f Manager::getVector2 (const std::string& setting, const std::string& c stream >> x >> y; if (stream.fail()) throw std::runtime_error(std::string("Can't parse 2d vector: " + value)); - return osg::Vec2f(x, y); + return {x, y}; } osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& category) @@ -104,14 +104,14 @@ osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& c stream >> x >> y >> z; if (stream.fail()) throw std::runtime_error(std::string("Can't parse 3d vector: " + value)); - return osg::Vec3f(x, y, z); + return {x, y, z}; } void Manager::setString(const std::string &setting, const std::string &category, const std::string &value) { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::key_type key (category, setting); - CategorySettingValueMap::iterator found = mUserSettings.find(key); + auto found = mUserSettings.find(key); if (found != mUserSettings.end()) { if (found->second == value) @@ -165,18 +165,35 @@ void Manager::setVector3 (const std::string &setting, const std::string &categor void Manager::resetPendingChange(const std::string &setting, const std::string &category) { - CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::key_type key (category, setting); mChangedSettings.erase(key); } -const CategorySettingVector Manager::getPendingChanges() +CategorySettingVector Manager::getPendingChanges() { return mChangedSettings; } +CategorySettingVector Manager::getPendingChanges(const CategorySettingVector& filter) +{ + CategorySettingVector intersection; + std::set_intersection(mChangedSettings.begin(), mChangedSettings.end(), + filter.begin(), filter.end(), + std::inserter(intersection, intersection.begin())); + return intersection; +} + void Manager::resetPendingChanges() { mChangedSettings.clear(); } +void Manager::resetPendingChanges(const CategorySettingVector& filter) +{ + for (const auto& key : filter) + { + mChangedSettings.erase(key); + } +} + } diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index e3a29d4c34..21d5aff770 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -36,11 +36,19 @@ namespace Settings ///< save user settings to file static void resetPendingChange(const std::string &setting, const std::string &category); + ///< resets a single pending change static void resetPendingChanges(); + ///< resets the list of all pending changes - static const CategorySettingVector getPendingChanges(); - ///< returns the list of changed settings and then clears it + static void resetPendingChanges(const CategorySettingVector& filter); + ///< resets only the pending changes listed in the filter + + static CategorySettingVector getPendingChanges(); + ///< returns the list of changed settings + + static CategorySettingVector getPendingChanges(const CategorySettingVector& filter); + ///< returns the list of changed settings intersecting with the filter static int getInt (const std::string& setting, const std::string& category); static float getFloat (const std::string& setting, const std::string& category); @@ -50,15 +58,15 @@ namespace Settings static osg::Vec2f getVector2 (const std::string& setting, const std::string& category); static osg::Vec3f getVector3 (const std::string& setting, const std::string& category); - static void setInt (const std::string& setting, const std::string& category, const int value); - static void setFloat (const std::string& setting, const std::string& category, const float value); - static void setDouble (const std::string& setting, const std::string& category, const double value); + static void setInt (const std::string& setting, const std::string& category, int value); + static void setFloat (const std::string& setting, const std::string& category, float value); + static void setDouble (const std::string& setting, const std::string& category, double value); static void setString (const std::string& setting, const std::string& category, const std::string& value); - static void setBool (const std::string& setting, const std::string& category, const bool value); - static void setVector2 (const std::string& setting, const std::string& category, const osg::Vec2f value); - static void setVector3 (const std::string& setting, const std::string& category, const osg::Vec3f value); + static void setBool (const std::string& setting, const std::string& category, bool value); + static void setVector2 (const std::string& setting, const std::string& category, osg::Vec2f value); + static void setVector3 (const std::string& setting, const std::string& category, osg::Vec3f value); }; } -#endif // _COMPONENTS_SETTINGS_H +#endif // COMPONENTS_SETTINGS_H diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index e057cfac02..30c7ed1b1d 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -345,7 +345,7 @@ namespace Shader if (found == mPrograms.end()) { if (!programTemplate) programTemplate = mProgramTemplate; - osg::ref_ptr program = programTemplate ? static_cast(programTemplate->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; + osg::ref_ptr program = programTemplate ? cloneProgram(programTemplate) : osg::ref_ptr(new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; @@ -353,6 +353,14 @@ namespace Shader return found->second; } + osg::ref_ptr ShaderManager::cloneProgram(const osg::Program* src) + { + osg::ref_ptr program = static_cast(src->clone(osg::CopyOp::SHALLOW_COPY)); + for (auto [name, idx] : src->getUniformBlockBindingList()) + program->addBindUniformBlock(name, idx); + return program; + } + ShaderManager::DefineMap ShaderManager::getGlobalDefines() { return DefineMap(mGlobalDefines); diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index d0ee069b10..613f33b168 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -38,6 +38,9 @@ namespace Shader const osg::Program* getProgramTemplate() const { return mProgramTemplate; } void setProgramTemplate(const osg::Program* program) { mProgramTemplate = program; } + /// Clone an osg::Program including bindUniformBlocks that osg::Program::clone does not copy for some reason. + static osg::ref_ptr cloneProgram(const osg::Program*); + /// Get (a copy of) the DefineMap used to construct all shaders DefineMap getGlobalDefines(); diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 6709ee842e..95e0a75f18 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -11,6 +11,8 @@ #include #include +#include + #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include "removedalphafunc.hpp" #include "shadermanager.hpp" @@ -516,6 +519,14 @@ namespace Shader // We could fall back to a texture size uniform if EXT_gpu_shader4 is missing } + bool simpleLighting = false; + node.getUserValue("simpleLighting", simpleLighting); + if (simpleLighting) + { + defineMap["forcePPL"] = "1"; + defineMap["endLight"] = "0"; + } + if (writableStateSet->getMode(GL_ALPHA_TEST) != osg::StateAttribute::INHERIT && !previousAddedState->hasMode(GL_ALPHA_TEST)) removedState->setMode(GL_ALPHA_TEST, writableStateSet->getMode(GL_ALPHA_TEST)); // This disables the deprecated fixed-function alpha test @@ -546,6 +557,27 @@ namespace Shader updateAddedState(*writableUserData, addedState); } + bool softParticles = false; + + if (mOpaqueDepthTex) + { + auto partsys = dynamic_cast(&node); + + if (partsys) + { + softParticles = true; + + auto depth = new SceneUtil::AutoDepth; + depth->setWriteMask(false); + writableStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + writableStateSet->addUniform(new osg::Uniform("particleSize", partsys->getDefaultParticleTemplate().getSizeRange().maximum)); + writableStateSet->addUniform(new osg::Uniform("opaqueDepthTex", 2)); + writableStateSet->setTextureAttributeAndModes(2, mOpaqueDepthTex, osg::StateAttribute::ON); + } + } + + defineMap["softParticles"] = softParticles ? "1" : "0"; + std::string shaderPrefix; if (!node.getUserValue("shaderPrefix", shaderPrefix)) shaderPrefix = mDefaultShaderPrefix; @@ -761,6 +793,11 @@ namespace Shader mConvertAlphaTestToAlphaToCoverage = convert; } + void ShaderVisitor::setOpaqueDepthTex(osg::ref_ptr texture) + { + mOpaqueDepthTex = texture; + } + ReinstateRemovedStateVisitor::ReinstateRemovedStateVisitor(bool allowedToModifyStateSets) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAllowedToModifyStateSets(allowedToModifyStateSets) diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index 5f9739ea90..d80e697fd8 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace Resource { @@ -45,6 +46,8 @@ namespace Shader void setConvertAlphaTestToAlphaToCoverage(bool convert); + void setOpaqueDepthTex(osg::ref_ptr texture); + void apply(osg::Node& node) override; void apply(osg::Drawable& drawable) override; @@ -110,6 +113,7 @@ namespace Shader bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs); osg::ref_ptr mProgramTemplate; + osg::ref_ptr mOpaqueDepthTex; }; class ReinstateRemovedStateVisitor : public osg::NodeVisitor diff --git a/components/sqlite3/db.cpp b/components/sqlite3/db.cpp new file mode 100644 index 0000000000..54a156057d --- /dev/null +++ b/components/sqlite3/db.cpp @@ -0,0 +1,31 @@ +#include "db.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3::operator()(sqlite3* handle) const noexcept + { + sqlite3_close(handle); + } + + Db makeDb(std::string_view path, const char* schema) + { + sqlite3* handle = nullptr; + const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + if (const int ec = sqlite3_open_v2(std::string(path).c_str(), &handle, flags, nullptr); ec != SQLITE_OK) + { + const std::string message(sqlite3_errmsg(handle)); + sqlite3_close(handle); + throw std::runtime_error("Failed to open database: " + message); + } + Db result(handle); + if (const int ec = sqlite3_exec(result.get(), schema, nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed create database schema: " + std::string(sqlite3_errmsg(handle))); + return result; + } +} diff --git a/components/sqlite3/db.hpp b/components/sqlite3/db.hpp new file mode 100644 index 0000000000..293ace4375 --- /dev/null +++ b/components/sqlite3/db.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_DB_H +#define OPENMW_COMPONENTS_SQLITE3_DB_H + +#include +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct CloseSqlite3 + { + void operator()(sqlite3* handle) const noexcept; + }; + + using Db = std::unique_ptr; + + Db makeDb(std::string_view path, const char* schema); +} + +#endif diff --git a/components/sqlite3/request.hpp b/components/sqlite3/request.hpp new file mode 100644 index 0000000000..339c7f7521 --- /dev/null +++ b/components/sqlite3/request.hpp @@ -0,0 +1,276 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_REQUEST_H +#define OPENMW_COMPONENTS_SQLITE3_REQUEST_H + +#include "statement.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Sqlite3 +{ + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, int value) + { + if (const int ec = sqlite3_bind_int(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::int64_t value) + { + if (const int ec = sqlite3_bind_int64(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind int64 to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, double value) + { + if (const int ec = sqlite3_bind_double(&stmt, index, value); ec != SQLITE_OK) + throw std::runtime_error("Failed to bind double to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, std::string_view value) + { + if (sqlite3_bind_text(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind text to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, int index, const std::vector& value) + { + if (sqlite3_bind_blob(&stmt, index, value.data(), static_cast(value.size()), SQLITE_STATIC) != SQLITE_OK) + throw std::runtime_error("Failed to bind blob to parameter " + std::to_string(index) + + ": " + std::string(sqlite3_errmsg(&db))); + } + + template + inline void bindParameter(sqlite3& db, sqlite3_stmt& stmt, const char* name, const T& value) + { + const int index = sqlite3_bind_parameter_index(&stmt, name); + if (index == 0) + throw std::logic_error("Parameter \"" + std::string(name) + "\" is not found"); + bindParameter(db, stmt, index, value); + } + + inline std::string sqliteTypeToString(int value) + { + switch (value) + { + case SQLITE_INTEGER: return "SQLITE_INTEGER"; + case SQLITE_FLOAT: return "SQLITE_FLOAT"; + case SQLITE_TEXT: return "SQLITE_TEXT"; + case SQLITE_BLOB: return "SQLITE_BLOB"; + case SQLITE_NULL: return "SQLITE_NULL"; + } + return "unsupported(" + std::to_string(value) + ")"; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& /*statement*/, int index, int type, T*& value) + { + if (type != SQLITE_NULL) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT"); + value = nullptr; + } + + template + inline auto copyColumn(sqlite3& /*db*/, sqlite3_stmt& statement, int index, int type, T& value) + { + switch (type) + { + case SQLITE_INTEGER: + value = static_cast(sqlite3_column_int64(&statement, index)); + return; + case SQLITE_FLOAT: + value = static_cast(sqlite3_column_double(&statement, index)); + return; + case SQLITE_NULL: + value = std::decay_t{}; + return; + } + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_INTEGER or SQLITE_FLOAT or SQLITE_NULL"); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::string& value) + { + if (type != SQLITE_TEXT) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_TEXT"); + const unsigned char* const text = sqlite3_column_text(&statement, index); + if (text == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read text from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(reinterpret_cast(text), reinterpret_cast(text) + size); + } + + inline void copyColumn(sqlite3& db, sqlite3_stmt& statement, int index, int type, std::vector& value) + { + if (type != SQLITE_BLOB) + throw std::logic_error("Type of column " + std::to_string(index) + " is " + sqliteTypeToString(type) + + " that does not match expected output type: SQLITE_BLOB"); + const void* const blob = sqlite3_column_blob(&statement, index); + if (blob == nullptr) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to read blob from column " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + const int size = sqlite3_column_bytes(&statement, index); + if (size <= 0) + { + if (const int ec = sqlite3_errcode(&db); ec != SQLITE_OK) + throw std::runtime_error("Failed to get column bytes " + std::to_string(index) + + ": " + sqlite3_errmsg(&db)); + value.clear(); + return; + } + value.reserve(static_cast(size)); + value.assign(static_cast(blob), static_cast(blob) + size); + } + + template + inline void getColumnsImpl(sqlite3& db, sqlite3_stmt& statement, T& row) + { + if constexpr (0 < index && index <= std::tuple_size_v) + { + const int column = index - 1; + if (const int number = sqlite3_column_count(&statement); column >= number) + throw std::out_of_range("Column number is out of range: " + std::to_string(column) + + " >= " + std::to_string(number)); + const int type = sqlite3_column_type(&statement, column); + switch (type) + { + case SQLITE_INTEGER: + case SQLITE_FLOAT: + case SQLITE_TEXT: + case SQLITE_BLOB: + case SQLITE_NULL: + copyColumn(db, statement, column, type, std::get(row)); + break; + default: + throw std::runtime_error("Column " + std::to_string(column) + + " has unnsupported column type: " + sqliteTypeToString(type)); + } + getColumnsImpl(db, statement, row); + } + } + + template + inline void getColumns(sqlite3& db, sqlite3_stmt& statement, T& row) + { + getColumnsImpl>(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, T& row) + { + auto tuple = std::tie(row); + getColumns(db, statement, tuple); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::tuple& row) + { + getColumns(db, statement, row); + } + + template + inline void getRow(sqlite3& db, sqlite3_stmt& statement, std::back_insert_iterator& it) + { + typename T::value_type row; + getRow(db, statement, row); + it = std::move(row); + } + + template + inline void prepare(sqlite3& db, Statement& statement, Args&& ... args) + { + if (statement.mNeedReset) + { + if (sqlite3_reset(statement.mHandle.get()) == SQLITE_OK + && sqlite3_clear_bindings(statement.mHandle.get()) == SQLITE_OK) + statement.mNeedReset = false; + else + statement.mHandle = makeStatementHandle(db, statement.mQuery.text()); + } + statement.mQuery.bind(db, *statement.mHandle, std::forward(args) ...); + } + + template + inline bool executeStep(sqlite3& db, const Statement& statement) + { + switch (sqlite3_step(statement.mHandle.get())) + { + case SQLITE_ROW: return true; + case SQLITE_DONE: return false; + } + throw std::runtime_error("Failed to execute statement step: " + std::string(sqlite3_errmsg(&db))); + } + + template + inline I request(sqlite3& db, Statement& statement, I out, std::size_t max, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + for (std::size_t i = 0; executeStep(db, statement) && i < max; ++i) + getRow(db, *statement.mHandle, *out++); + return out; + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed perform request \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } + + template + inline int execute(sqlite3& db, Statement& statement, Args&& ... args) + { + try + { + statement.mNeedReset = true; + prepare(db, statement, std::forward(args) ...); + if (executeStep(db, statement)) + throw std::logic_error("Execute cannot return rows"); + return sqlite3_changes(&db); + } + catch (const std::exception& e) + { + throw std::runtime_error("Failed to execute statement \"" + std::string(statement.mQuery.text()) + + "\": " + std::string(e.what())); + } + } +} + +#endif diff --git a/components/sqlite3/statement.cpp b/components/sqlite3/statement.cpp new file mode 100644 index 0000000000..07ca6b8ddf --- /dev/null +++ b/components/sqlite3/statement.cpp @@ -0,0 +1,24 @@ +#include "statement.hpp" + +#include + +#include +#include +#include + +namespace Sqlite3 +{ + void CloseSqlite3Stmt::operator()(sqlite3_stmt* handle) const noexcept + { + sqlite3_finalize(handle); + } + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query) + { + sqlite3_stmt* stmt = nullptr; + if (const int ec = sqlite3_prepare_v2(&db, query.data(), static_cast(query.size()), &stmt, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to prepare statement for query \"" + std::string(query) + "\": " + + std::string(sqlite3_errmsg(&db))); + return StatementHandle(stmt); + } +} diff --git a/components/sqlite3/statement.hpp b/components/sqlite3/statement.hpp new file mode 100644 index 0000000000..469e63933c --- /dev/null +++ b/components/sqlite3/statement.hpp @@ -0,0 +1,35 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_STATEMENT_H +#define OPENMW_COMPONENTS_SQLITE3_STATEMENT_H + +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +namespace Sqlite3 +{ + struct CloseSqlite3Stmt + { + void operator()(sqlite3_stmt* handle) const noexcept; + }; + + using StatementHandle = std::unique_ptr; + + StatementHandle makeStatementHandle(sqlite3& db, std::string_view query); + + template + struct Statement + { + bool mNeedReset = false; + StatementHandle mHandle; + Query mQuery; + + explicit Statement(sqlite3& db, Query query = Query {}) + : mHandle(makeStatementHandle(db, query.text())), + mQuery(std::move(query)) {} + }; +} + +#endif diff --git a/components/sqlite3/transaction.cpp b/components/sqlite3/transaction.cpp new file mode 100644 index 0000000000..3012538652 --- /dev/null +++ b/components/sqlite3/transaction.cpp @@ -0,0 +1,33 @@ +#include "transaction.hpp" + +#include + +#include + +#include +#include + +namespace Sqlite3 +{ + void Rollback::operator()(sqlite3* db) const + { + if (db == nullptr) + return; + if (const int ec = sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); ec != SQLITE_OK) + Log(Debug::Warning) << "Failed to rollback SQLite3 transaction: " << std::string(sqlite3_errmsg(db)); + } + + Transaction::Transaction(sqlite3& db) + : mDb(&db) + { + if (const int ec = sqlite3_exec(mDb.get(), "BEGIN", nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to start transaction: " + std::string(sqlite3_errmsg(mDb.get()))); + } + + void Transaction::commit() + { + if (const int ec = sqlite3_exec(mDb.get(), "COMMIT", nullptr, nullptr, nullptr); ec != SQLITE_OK) + throw std::runtime_error("Failed to commit transaction: " + std::string(sqlite3_errmsg(mDb.get()))); + (void) mDb.release(); + } +} diff --git a/components/sqlite3/transaction.hpp b/components/sqlite3/transaction.hpp new file mode 100644 index 0000000000..88b780a0a5 --- /dev/null +++ b/components/sqlite3/transaction.hpp @@ -0,0 +1,27 @@ +#ifndef OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H +#define OPENMW_COMPONENTS_SQLITE3_TRANSACTION_H + +#include + +struct sqlite3; + +namespace Sqlite3 +{ + struct Rollback + { + void operator()(sqlite3* handle) const; + }; + + class Transaction + { + public: + Transaction(sqlite3& db); + + void commit(); + + private: + std::unique_ptr mDb; + }; +} + +#endif diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp index 399df16d34..658c431b1b 100644 --- a/components/terrain/buffercache.cpp +++ b/components/terrain/buffercache.cpp @@ -12,8 +12,8 @@ namespace template osg::ref_ptr createIndexBuffer(unsigned int flags, unsigned int verts) { - // LOD level n means every 2^n-th vertex is kept - size_t lodLevel = (flags >> (4*4)); + // LOD level n means every 2^n-th vertex is kept, but we currently handle LOD elsewhere. + size_t lodLevel = 0;//(flags >> (4*4)); size_t lodDeltas[4]; for (int i=0; i<4; ++i) diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index e040cbdd93..d57b73768f 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -52,6 +52,9 @@ struct FindChunkTemplate osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { + // Override lod with the vertexLodMod adjusted value. + // TODO: maybe we can refactor this code by moving all vertexLodMod code into this class. + lod = static_cast(lodFlags >> (4*4)); ChunkId id = std::make_tuple(center, lod, lodFlags); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) diff --git a/components/terrain/compositemaprenderer.cpp b/components/terrain/compositemaprenderer.cpp index 2a3fa47daa..55da3d347a 100644 --- a/components/terrain/compositemaprenderer.cpp +++ b/components/terrain/compositemaprenderer.cpp @@ -4,9 +4,6 @@ #include #include -#include -#include - #include namespace Terrain @@ -21,8 +18,6 @@ CompositeMapRenderer::CompositeMapRenderer() mFBO = new osg::FrameBufferObject; - mUnrefQueue = new SceneUtil::UnrefQueue; - getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); } @@ -30,11 +25,6 @@ CompositeMapRenderer::~CompositeMapRenderer() { } -void CompositeMapRenderer::setWorkQueue(SceneUtil::WorkQueue* workQueue) -{ - mWorkQueue = workQueue; -} - void CompositeMapRenderer::drawImplementation(osg::RenderInfo &renderInfo) const { double dt = mTimer.time_s(); @@ -45,9 +35,6 @@ void CompositeMapRenderer::drawImplementation(osg::RenderInfo &renderInfo) const double availableTime = std::max((targetFrameTime - dt)*conservativeTimeRatio, mMinimumTimeAvailable); - if (mWorkQueue) - mUnrefQueue->flush(mWorkQueue.get()); - std::lock_guard lock(mMutex); if (mImmediateCompileSet.empty() && mCompileSet.empty()) @@ -139,10 +126,6 @@ void CompositeMapRenderer::compile(CompositeMap &compositeMap, osg::RenderInfo & ++compositeMap.mCompiled; - if (mWorkQueue) - { - mUnrefQueue->push(compositeMap.mDrawables[i]); - } compositeMap.mDrawables[i] = nullptr; if (timeLeft) diff --git a/components/terrain/compositemaprenderer.hpp b/components/terrain/compositemaprenderer.hpp index 257173af46..f0021c88fe 100644 --- a/components/terrain/compositemaprenderer.hpp +++ b/components/terrain/compositemaprenderer.hpp @@ -13,12 +13,6 @@ namespace osg class Texture2D; } -namespace SceneUtil -{ - class UnrefQueue; - class WorkQueue; -} - namespace Terrain { @@ -45,9 +39,6 @@ namespace Terrain void compile(CompositeMap& compositeMap, osg::RenderInfo& renderInfo, double* timeLeft) const; - /// Set a WorkQueue to delete compiled composite map layers in the background thread - void setWorkQueue(SceneUtil::WorkQueue* workQueue); - /// Set the available time in seconds for compiling (non-immediate) composite maps each frame void setMinimumTimeAvailableForCompile(double time); @@ -67,9 +58,6 @@ namespace Terrain double mMinimumTimeAvailable; mutable osg::Timer mTimer; - osg::ref_ptr mUnrefQueue; - osg::ref_ptr mWorkQueue; - typedef std::set > CompileSet; mutable CompileSet mCompileSet; diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 22f507b3a2..ae432d262e 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include @@ -87,7 +87,7 @@ namespace osg::ref_ptr mValue; EqualDepth() - : mValue(new osg::Depth) + : mValue(new SceneUtil::AutoDepth) { mValue->setFunction(osg::Depth::EQUAL); } @@ -106,7 +106,7 @@ namespace osg::ref_ptr mValue; LequalDepth() - : mValue(SceneUtil::createDepth()) + : mValue(new SceneUtil::AutoDepth) { } }; diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index c113a629c5..81d3ccb32d 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -10,6 +10,29 @@ namespace Terrain { +float distance(const osg::BoundingBox& box, const osg::Vec3f& v) +{ + if (box.contains(v)) + return 0; + else + { + osg::Vec3f maxDist(0,0,0); + if (v.x() < box.xMin()) + maxDist.x() = box.xMin() - v.x(); + else if (v.x() > box.xMax()) + maxDist.x() = v.x() - box.xMax(); + if (v.y() < box.yMin()) + maxDist.y() = box.yMin() - v.y(); + else if (v.y() > box.yMax()) + maxDist.y() = v.y() - box.yMax(); + if (v.z() < box.zMin()) + maxDist.z() = box.zMin() - v.z(); + else if (v.z() > box.zMax()) + maxDist.z() = v.z() - box.zMax(); + return maxDist.length(); + } +} + ChildDirection reflect(ChildDirection dir, Direction dir2) { assert(dir != Root); @@ -78,25 +101,7 @@ QuadTreeNode *QuadTreeNode::getNeighbour(Direction dir) float QuadTreeNode::distance(const osg::Vec3f& v) const { const osg::BoundingBox& box = getBoundingBox(); - if (box.contains(v)) - return 0; - else - { - osg::Vec3f maxDist(0,0,0); - if (v.x() < box.xMin()) - maxDist.x() = box.xMin() - v.x(); - else if (v.x() > box.xMax()) - maxDist.x() = v.x() - box.xMax(); - if (v.y() < box.yMin()) - maxDist.y() = box.yMin() - v.y(); - else if (v.y() > box.yMax()) - maxDist.y() = v.y() - box.yMax(); - if (v.z() < box.zMin()) - maxDist.z() = box.zMin() - v.z(); - else if (v.z() > box.zMax()) - maxDist.z() = v.z() - box.zMax(); - return maxDist.length(); - } + return Terrain::distance(box, v); } void QuadTreeNode::initNeighbours() diff --git a/components/terrain/quadtreenode.hpp b/components/terrain/quadtreenode.hpp index 7ae59b92f0..aba78b8b3a 100644 --- a/components/terrain/quadtreenode.hpp +++ b/components/terrain/quadtreenode.hpp @@ -34,6 +34,8 @@ namespace Terrain class ViewData; + float distance(const osg::BoundingBox&, const osg::Vec3f& v); + class QuadTreeNode : public osg::Group { public: diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index 7e5cd001ba..6dbed34331 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -3,12 +3,12 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -40,9 +40,9 @@ namespace return 1 << depth; } - int Log2( unsigned int n ) + unsigned int Log2( unsigned int n ) { - int targetlevel = 0; + unsigned int targetlevel = 0; while (n >>= 1) ++targetlevel; return targetlevel; } @@ -78,16 +78,18 @@ public: if (intersects) return Deeper; } - dist = std::max(0.f, dist + mDistanceModifier); - if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded return StopTraversal; - - int nativeLodLevel = Log2(static_cast(node->getSize()/mMinSize)); - int lodLevel = Log2(static_cast(dist/(Constants::CellSizeInUnits*mMinSize*mFactor))); - - return nativeLodLevel <= lodLevel ? StopTraversalAndUse : Deeper; + return getNativeLodLevel(node, mMinSize) <= convertDistanceToLodLevel(dist, mMinSize, mFactor) ? StopTraversalAndUse : Deeper; + } + static unsigned int getNativeLodLevel(const QuadTreeNode* node, float minSize) + { + return Log2(static_cast(node->getSize()/minSize)); + } + static unsigned int convertDistanceToLodLevel(float dist, float minSize, float factor) + { + return Log2(static_cast(dist/(Constants::CellSizeInUnits*minSize*factor))); } private: @@ -257,10 +259,10 @@ public: { osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 }; auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size, mStorage, mSceneManager, mNodeMask, 5.f, { 1, 0, 0, 0 }); - osg::ref_ptr trans = new osg::MatrixTransform(osg::Matrixf::translate(-center*Constants::CellSizeInUnits)); - trans->setDataVariance(osg::Object::STATIC); - trans->addChild(chunkBorder); - return trans; + osg::ref_ptr pat = new SceneUtil::PositionAttitudeTransform; + pat->setPosition(-center*Constants::CellSizeInUnits); + pat->addChild(chunkBorder); + return pat; } unsigned int getNodeMask() { return mNodeMask; } private: @@ -295,13 +297,14 @@ QuadTreeWorld::~QuadTreeWorld() { } -/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the data set. +/// get the level of vertex detail to render this node at, expressed relative to the native resolution of the vertex data set, +/// NOT relative to mMinSize as is the case with node LODs. unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod) { - int lod = Log2(int(node->getSize())); + unsigned int vertexLod = DefaultLodCallback::getNativeLodLevel(node, 1); if (vertexLodMod > 0) { - lod = std::max(0, lod-vertexLodMod); + vertexLod = static_cast(std::max(0, static_cast(vertexLod)-vertexLodMod)); } else if (vertexLodMod < 0) { @@ -312,13 +315,13 @@ unsigned int getVertexLod(QuadTreeNode* node, int vertexLodMod) size *= 2; vertexLodMod = std::min(0, vertexLodMod+1); } - lod += std::abs(vertexLodMod); + vertexLod += std::abs(vertexLodMod); } - return lod; + return vertexLod; } /// get the flags to use for stitching in the index buffer so that chunks of different LOD connect seamlessly -unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const ViewData* vd) +unsigned int getLodFlags(QuadTreeNode* node, unsigned int ourVertexLod, int vertexLodMod, const ViewData* vd) { unsigned int lodFlags = 0; for (unsigned int i=0; i<4; ++i) @@ -331,32 +334,33 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const // our detail and the neighbour would handle stitching by itself. while (neighbour && !vd->contains(neighbour)) neighbour = neighbour->getParent(); - int lod = 0; + unsigned int lod = 0; if (neighbour) lod = getVertexLod(neighbour, vertexLodMod); - if (lod <= ourLod) // We only need to worry about neighbours less detailed than we are - + if (lod <= ourVertexLod) // We only need to worry about neighbours less detailed than we are - lod = 0; // neighbours with more detail will do the stitching themselves // Use 4 bits for each LOD delta if (lod > 0) { - lodFlags |= static_cast(lod - ourLod) << (4*i); + lodFlags |= (lod - ourVertexLod) << (4*i); } } + // Use the remaining bits for our vertex LOD + lodFlags |= (ourVertexLod << (4*4)); return lodFlags; } -void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile, float reuseDistance) +void QuadTreeWorld::loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile) { if (!vd->hasChanged() && entry.mRenderingNode) return; - int ourLod = getVertexLod(entry.mNode, vertexLodMod); - if (vd->hasChanged()) { + unsigned int ourVertexLod = getVertexLod(entry.mNode, mVertexLodMod); // have to recompute the lodFlags in case a neighbour has changed LOD. - unsigned int lodFlags = getLodFlags(entry.mNode, ourLod, vertexLodMod, vd); + unsigned int lodFlags = getLodFlags(entry.mNode, ourVertexLod, mVertexLodMod, vd); if (lodFlags != entry.mLodFlags) { entry.mRenderingNode = nullptr; @@ -372,11 +376,9 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f const osg::Vec2f& center = entry.mNode->getCenter(); bool activeGrid = (center.x() > gridbounds.x() && center.y() > gridbounds.y() && center.x() < gridbounds.z() && center.y() < gridbounds.w()); - for (QuadTreeWorld::ChunkManager* m : chunkManagers) + for (QuadTreeWorld::ChunkManager* m : mChunkManagers) { - if (m->getViewDistance() && entry.mNode->distance(vd->getViewPoint()) > m->getViewDistance() + reuseDistance) - continue; - osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), ourLod, entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); + osg::ref_ptr n = m->getChunk(entry.mNode->getSize(), entry.mNode->getCenter(), DefaultLodCallback::getNativeLodLevel(entry.mNode, mMinSize), entry.mLodFlags, activeGrid, vd->getViewPoint(), compile); if (n) pat->addChild(n); } entry.mRenderingNode = pat; @@ -398,7 +400,7 @@ void updateWaterCullingView(HeightCullCallback* callback, ViewData* vd, osgUtil: static bool debug = getenv("OPENMW_WATER_CULLING_DEBUG") != nullptr; for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); osg::BoundingBox bb = static_cast(entry.mRenderingNode->asGroup()->getChild(0))->getWaterBoundingBox(); if (!bb.valid()) continue; @@ -440,19 +442,7 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) { bool isCullVisitor = nv.getVisitorType() == osg::NodeVisitor::CULL_VISITOR; if (!isCullVisitor && nv.getVisitorType() != osg::NodeVisitor::INTERSECTION_VISITOR) - { - if (nv.getName().find("AcceptedByComponentsTerrainQuadTreeWorld") != std::string::npos) - { - if (nv.getName().find("SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds") != std::string::npos) - { - SceneUtil::MWShadowTechnique::ComputeLightSpaceBounds* clsb = static_cast(&nv); - clsb->apply(*this); - } - else - nv.apply(*mRootNode); - } return; - } osg::Object * viewer = isCullVisitor ? static_cast(&nv)->getCurrentCamera() : nullptr; bool needsUpdate = true; @@ -469,15 +459,15 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv) for (unsigned int i=0; igetNumEntries(); ++i) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, mActiveGrid, mChunkManagers, false, mViewDataMap->getReuseDistance()); + ViewDataEntry& entry = vd->getEntry(i); + loadRenderingNode(entry, vd, cellWorldSize, mActiveGrid, false); entry.mRenderingNode->accept(nv); } if (mHeightCullCallback && isCullVisitor) updateWaterCullingView(mHeightCullCallback, vd, static_cast(&nv), mStorage->getCellWorldSize(), !isGridEmpty()); - vd->markUnchanged(); + vd->setChanged(false); double referenceTime = nv.getFrameStamp() ? nv.getFrameStamp()->getReferenceTime() : 0.0; if (referenceTime != 0.0) @@ -510,9 +500,8 @@ void QuadTreeWorld::enable(bool enabled) if (!mRootNode->getNumParents()) mTerrainRoot->addChild(mRootNode); } - - if (mRootNode) - mRootNode->setNodeMask(enabled ? ~0 : 0); + else if (mRootNode) + mTerrainRoot->removeChild(mRootNode); } View* QuadTreeWorld::createView() @@ -550,12 +539,11 @@ void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg:: reporter.addTotal(progressTotal); } - const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier)); for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) { - ViewData::Entry& entry = vd->getEntry(i); + ViewDataEntry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, reuseDistance); + loadRenderingNode(entry, vd, cellWorldSize, grid, true); if (pass==0) reporter.addProgress(entry.mNode->getSize()); entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass } @@ -592,6 +580,8 @@ void QuadTreeWorld::addChunkManager(QuadTreeWorld::ChunkManager* m) { mChunkManagers.push_back(m); mTerrainRoot->setNodeMask(mTerrainRoot->getNodeMask()|m->getNodeMask()); + if (m->getViewDistance()) + m->setMaxLodLevel(DefaultLodCallback::convertDistanceToLodLevel(m->getViewDistance() + mViewDataMap->getReuseDistance(), mMinSize, mLodFactor)); } void QuadTreeWorld::rebuildViews() @@ -599,4 +589,12 @@ void QuadTreeWorld::rebuildViews() mViewDataMap->rebuildViews(); } +void QuadTreeWorld::setViewDistance(float viewDistance) +{ + if (mViewDistance == viewDistance) + return; + mViewDistance = viewDistance; + mViewDataMap->rebuildViews(); +} + } diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 3bd606d6c6..3748f7df98 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -16,6 +16,9 @@ namespace Terrain { class RootNode; class ViewDataMap; + class ViewData; + struct ViewDataEntry; + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. @@ -30,7 +33,7 @@ namespace Terrain void enable(bool enabled) override; - void setViewDistance(float distance) override { mViewDistance = distance; } + void setViewDistance(float distance) override; void cacheCell(View *view, int x, int y) override {} /// @note Not thread safe. @@ -53,13 +56,19 @@ namespace Terrain void setViewDistance(float viewDistance) { mViewDistance = viewDistance; } float getViewDistance() const { return mViewDistance; } + + // Automatically set by addChunkManager based on getViewDistance() + unsigned int getMaxLodLevel() const { return mMaxLodLevel; } + void setMaxLodLevel(unsigned int level) { mMaxLodLevel = level; } private: float mViewDistance = 0.f; + unsigned int mMaxLodLevel = ~0u; }; void addChunkManager(ChunkManager*); private: void ensureQuadTreeBuilt(); + void loadRenderingNode(ViewDataEntry& entry, ViewData* vd, float cellWorldSize, const osg::Vec4i &gridbounds, bool compile); osg::ref_ptr mRootNode; diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index 3ebc99f1df..033b5f5faa 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -12,12 +12,10 @@ ViewData::ViewData() , mHasViewPoint(false) , mWorldUpdateRevision(0) { - } ViewData::~ViewData() { - } void ViewData::copyFrom(const ViewData& other) @@ -38,42 +36,19 @@ void ViewData::add(QuadTreeNode *node) if (index+1 > mEntries.size()) mEntries.resize(index+1); - Entry& entry = mEntries[index]; + ViewDataEntry& entry = mEntries[index]; if (entry.set(node)) mChanged = true; } -unsigned int ViewData::getNumEntries() const -{ - return mNumEntries; -} - -ViewData::Entry &ViewData::getEntry(unsigned int i) -{ - return mEntries[i]; -} - -bool ViewData::hasChanged() const -{ - return mChanged; -} - -bool ViewData::hasViewPoint() const -{ - return mHasViewPoint; -} - void ViewData::setViewPoint(const osg::Vec3f &viewPoint) { mViewPoint = viewPoint; mHasViewPoint = true; } -const osg::Vec3f& ViewData::getViewPoint() const -{ - return mViewPoint; -} - +// NOTE: As a performance optimisation, we cache mRenderingNodes from previous frames here. +// If this cache becomes invalid (e.g. through mWorldUpdateRevision), we need to use clear() instead of reset(). void ViewData::reset() { // clear any unused entries @@ -108,14 +83,13 @@ bool ViewData::contains(QuadTreeNode *node) const return false; } -ViewData::Entry::Entry() +ViewDataEntry::ViewDataEntry() : mNode(nullptr) , mLodFlags(0) { - } -bool ViewData::Entry::set(QuadTreeNode *node) +bool ViewDataEntry::set(QuadTreeNode *node) { if (node == mNode) return false; @@ -164,9 +138,13 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo } else if (!mostSuitableView) { + if (vd->getWorldUpdateRevision() != mWorldUpdateRevision) + { + vd->setWorldUpdateRevision(mWorldUpdateRevision); + vd->clear(); + } vd->setViewPoint(viewPoint); vd->setActiveGrid(activeGrid); - vd->setWorldUpdateRevision(mWorldUpdateRevision); needsUpdate = true; } } diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 5d814251ea..d51da011a2 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -13,6 +13,18 @@ namespace Terrain class QuadTreeNode; + struct ViewDataEntry + { + ViewDataEntry(); + + bool set(QuadTreeNode* node); + + QuadTreeNode* mNode; + + unsigned int mLodFlags; + osg::ref_ptr mRenderingNode; + }; + class ViewData : public View { public: @@ -31,33 +43,22 @@ namespace Terrain void copyFrom(const ViewData& other); - struct Entry - { - Entry(); - - bool set(QuadTreeNode* node); - - QuadTreeNode* mNode; - - unsigned int mLodFlags; - osg::ref_ptr mRenderingNode; - }; - - unsigned int getNumEntries() const; - - Entry& getEntry(unsigned int i); + unsigned int getNumEntries() const { return mNumEntries; } + ViewDataEntry& getEntry(unsigned int i) { return mEntries[i]; } double getLastUsageTimeStamp() const { return mLastUsageTimeStamp; } void setLastUsageTimeStamp(double timeStamp) { mLastUsageTimeStamp = timeStamp; } - /// @return Have any nodes changed since the last frame - bool hasChanged() const; - void markUnchanged() { mChanged = false; } + /// Indicates at least one mNode of mEntries has changed. + /// @note Such changes may necessitate a revalidation of cached mRenderingNodes elsewhere depending + /// on the parameters that affect the creation of mRenderingNode. + bool hasChanged() const { return mChanged; } + void setChanged(bool changed) { mChanged = changed; } - bool hasViewPoint() const; + bool hasViewPoint() const { return mHasViewPoint; } void setViewPoint(const osg::Vec3f& viewPoint); - const osg::Vec3f& getViewPoint() const; + const osg::Vec3f& getViewPoint() const { return mViewPoint; } void setActiveGrid(const osg::Vec4i &grid) { if (grid != mActiveGrid) {mActiveGrid = grid;mEntries.clear();mNumEntries=0;} } const osg::Vec4i &getActiveGrid() const { return mActiveGrid;} @@ -66,7 +67,7 @@ namespace Terrain void setWorldUpdateRevision(int updateRevision) { mWorldUpdateRevision = updateRevision; } private: - std::vector mEntries; + std::vector mEntries; unsigned int mNumEntries; double mLastUsageTimeStamp; bool mChanged; diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 17a51913e5..582cc68b1b 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -84,11 +84,6 @@ World::~World() } } -void World::setWorkQueue(SceneUtil::WorkQueue* workQueue) -{ - mCompositeMapRenderer->setWorkQueue(workQueue); -} - void World::setBordersVisible(bool visible) { mBorderVisible = visible; diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index def7a2eccf..b9fa7c0160 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -28,11 +28,6 @@ namespace Resource class ResourceSystem; } -namespace SceneUtil -{ - class WorkQueue; -} - namespace Loading { class Reporter; @@ -115,9 +110,6 @@ namespace Terrain World(osg::Group* parent, Storage* storage, unsigned int nodeMask); virtual ~World(); - /// Set a WorkQueue to delete objects in the background thread. - void setWorkQueue(SceneUtil::WorkQueue* workQueue); - /// See CompositeMapRenderer::setTargetFrameRate void setTargetFrameRate(float rate); diff --git a/components/vfs/bsaarchive.cpp b/components/vfs/bsaarchive.cpp index 90899ac612..b0caa5c305 100644 --- a/components/vfs/bsaarchive.cpp +++ b/components/vfs/bsaarchive.cpp @@ -1,21 +1,14 @@ #include "bsaarchive.hpp" -#include + #include +#include namespace VFS { BsaArchive::BsaArchive(const std::string &filename) { - Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(filename); - - if (bsaVersion == Bsa::BSAVER_COMPRESSED) { - mFile = std::make_unique(Bsa::CompressedBSAFile()); - } - else { - mFile = std::make_unique(Bsa::BSAFile()); - } - + mFile = std::make_unique(Bsa::BSAFile()); mFile->open(filename); const Bsa::BSAFile::FileList &filelist = mFile->getList(); @@ -25,6 +18,10 @@ BsaArchive::BsaArchive(const std::string &filename) } } +BsaArchive::BsaArchive() +{ +} + BsaArchive::~BsaArchive() { } @@ -56,6 +53,31 @@ std::string BsaArchive::getDescription() const return std::string{"BSA: "} + mFile->getFilename(); } +CompressedBsaArchive::CompressedBsaArchive(const std::string &filename) + : BsaArchive() +{ + mFile = std::make_unique(Bsa::CompressedBSAFile()); + mFile->open(filename); + + const Bsa::BSAFile::FileList &filelist = mFile->getList(); + for(Bsa::BSAFile::FileList::const_iterator it = filelist.begin();it != filelist.end();++it) + { + mResources.emplace_back(&*it, mFile.get()); + mCompressedResources.emplace_back(&*it, static_cast(mFile.get())); + } +} + +void CompressedBsaArchive::listResources(std::map &out, char (*normalize_function)(char)) +{ + for (std::vector::iterator it = mCompressedResources.begin(); it != mCompressedResources.end(); ++it) + { + std::string ent = it->mInfo->name(); + std::transform(ent.begin(), ent.end(), ent.begin(), normalize_function); + + out[ent] = &*it; + } +} + // ------------------------------------------------------------------------------ BsaArchiveFile::BsaArchiveFile(const Bsa::BSAFile::FileStruct *info, Bsa::BSAFile* bsa) @@ -70,4 +92,16 @@ Files::IStreamPtr BsaArchiveFile::open() return mFile->getFile(mInfo); } +CompressedBsaArchiveFile::CompressedBsaArchiveFile(const Bsa::BSAFile::FileStruct *info, Bsa::CompressedBSAFile* bsa) + : BsaArchiveFile(info, bsa) + , mCompressedFile(bsa) +{ + +} + +Files::IStreamPtr CompressedBsaArchiveFile::open() +{ + return mCompressedFile->getFile(mInfo); +} + } diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index c979b5ce7e..1015f8d220 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -4,6 +4,7 @@ #include "archive.hpp" #include +#include namespace VFS { @@ -18,19 +19,43 @@ namespace VFS Bsa::BSAFile* mFile; }; + class CompressedBsaArchiveFile : public BsaArchiveFile + { + public: + CompressedBsaArchiveFile(const Bsa::BSAFile::FileStruct* info, Bsa::CompressedBSAFile* bsa); + + Files::IStreamPtr open() override; + Bsa::CompressedBSAFile* mCompressedFile; + }; + + class BsaArchive : public Archive { public: BsaArchive(const std::string& filename); + BsaArchive(); virtual ~BsaArchive(); void listResources(std::map& out, char (*normalize_function) (char)) override; bool contains(const std::string& file, char (*normalize_function) (char)) const override; std::string getDescription() const override; - private: + protected: std::unique_ptr mFile; std::vector mResources; }; + + class CompressedBsaArchive : public BsaArchive + { + public: + CompressedBsaArchive(const std::string& filename); + void listResources(std::map& out, char (*normalize_function) (char)) override; + virtual ~CompressedBsaArchive() {} + + private: + std::unique_ptr mCompressedFile; + std::vector mCompressedResources; + }; + } #endif diff --git a/components/vfs/registerarchives.cpp b/components/vfs/registerarchives.cpp index 80e639f350..4dc44fb25e 100644 --- a/components/vfs/registerarchives.cpp +++ b/components/vfs/registerarchives.cpp @@ -23,8 +23,12 @@ namespace VFS // Last BSA has the highest priority const std::string archivePath = collections.getPath(*archive).string(); Log(Debug::Info) << "Adding BSA archive " << archivePath; + Bsa::BsaVersion bsaVersion = Bsa::CompressedBSAFile::detectVersion(archivePath); - vfs->addArchive(new BsaArchive(archivePath)); + if (bsaVersion == Bsa::BSAVER_COMPRESSED) + vfs->addArchive(new CompressedBsaArchive(archivePath)); + else + vfs->addArchive(new BsaArchive(archivePath)); } else { diff --git a/components/widgets/fontwrapper.hpp b/components/widgets/fontwrapper.hpp index daa69f9202..16ebba3587 100644 --- a/components/widgets/fontwrapper.hpp +++ b/components/widgets/fontwrapper.hpp @@ -31,15 +31,11 @@ namespace Gui } private: - static int clamp(const int& value, const int& lowBound, const int& highBound) - { - return std::min(std::max(lowBound, value), highBound); - } std::string getFontSize() { // Note: we can not use the FontLoader here, so there is a code duplication a bit. - static const std::string fontSize = std::to_string(clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); + static const std::string fontSize = std::to_string(std::clamp(Settings::Manager::getInt("font size", "GUI"), 12, 20)); return fontSize; } }; diff --git a/components/widgets/numericeditbox.cpp b/components/widgets/numericeditbox.cpp index e8ba226f70..c6ff9628ee 100644 --- a/components/widgets/numericeditbox.cpp +++ b/components/widgets/numericeditbox.cpp @@ -31,7 +31,7 @@ namespace Gui try { mValue = std::stoi(newCaption); - int capped = std::min(mMaxValue, std::max(mValue, mMinValue)); + int capped = std::clamp(mValue, mMinValue, mMaxValue); if (capped != mValue) { mValue = capped; diff --git a/docs/requirements.txt b/docs/requirements.txt index 288d462d0d..ac82149f5d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ parse_cmake -sphinx>=1.7.0 +sphinx==1.8.5 +docutils==0.17.1 diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh index 7a238eca5a..067a1ad4cf 100755 --- a/docs/source/generate_luadoc.sh +++ b/docs/source/generate_luadoc.sh @@ -8,6 +8,18 @@ # luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec # luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +# How to install on Windows: + +# install LuaRocks (heavily recommended to use the standalone package) +# https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows +# git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git +# cd openmw-luadocumentor/luarocks +# open "Developer Command Prompt for VS <2017/2019>" in this directory and run: +# luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec +# luarocks --local install openmwluadocumentor-0.1.1-1.src.rock +# open "Git Bash" in the same directory and run script: +# ./generate_luadoc.sh + if [ -f /.dockerenv ]; then # We are inside readthedocs pipeline echo "Install lua 5.1" @@ -32,7 +44,6 @@ if [ -f /.dockerenv ]; then cd ~ git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git cd openmw-luadocumentor/luarocks - luarocks --local install checks luarocks --local pack openmwluadocumentor-0.1.1-1.rockspec luarocks --local install openmwluadocumentor-0.1.1-1.src.rock fi @@ -40,12 +51,19 @@ fi DOCS_SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" FILES_DIR=$DOCS_SOURCE_DIR/../../files OUTPUT_DIR=$DOCS_SOURCE_DIR/reference/lua-scripting/generated_html +DOCUMENTOR_PATH=~/.luarocks/bin/openmwluadocumentor + +if [ ! -x $DOCUMENTOR_PATH ]; then + # running on Windows? + DOCUMENTOR_PATH="$APPDATA/LuaRocks/bin/openmwluadocumentor.bat" +fi rm -f $OUTPUT_DIR/*.html cd $FILES_DIR/lua_api -~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua cd $FILES_DIR/builtin_scripts -~/.luarocks/bin/openmwluadocumentor -f doc -d $OUTPUT_DIR openmw_aux/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua +$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera.lua diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 538cfd4c6d..2af7946536 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -105,10 +105,10 @@ If you are running macOS, you can also download Morrowind through Steam: #. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` -Linux ----- -Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". ----- +Linux +----- +Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". +--------------------------------------------------------- #. Install Steam from "Ubuntu Software" Center #. Enable Proton (basically WINE under the hood). This is done in the Steam client menu drop down. Select, "Steam | Settings" then in the "SteamPlay" section check the box next to "enable steam play for all other titles" #. Now Morrowind should be selectable in your game list (as long as you own it). You can install it like any other game, choose to install it and remember the directory path of the location you pick. diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 75dbe8dca2..d2b67d02ca 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -154,9 +154,9 @@ A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" - and merge the changes you've made into the project master repository. One of the project maintainers will probably ask - you to make some corrections or clarifications. Go back and repeat this process to make those changes, - and repeat until they're good enough to get merged. +and merge the changes you've made into the project master repository. One of the project maintainers will probably ask +you to make some corrections or clarifications. Go back and repeat this process to make those changes, +and repeat until they're good enough to get merged. So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 5075f8a5fe..dd9d151482 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -6,6 +6,7 @@ Lua API reference :hidden: engine_handlers + user_interface openmw_util openmw_settings openmw_core @@ -16,10 +17,13 @@ Lua API reference openmw_nearby openmw_input openmw_ui + openmw_camera openmw_aux_util + interface_camera - :ref:`Engine handlers reference` +- :ref:`User interface reference ` - `Game object reference `_ - `Cell reference `_ @@ -54,11 +58,11 @@ Player scripts are local scripts that are attached to a player. +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.nearby ` | by local scripts | | Read-only access to the nearest area of the game world. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.input ` | by player scripts | | User input | +|:ref:`openmw.input ` | by player scripts | | User input. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.ui ` | by player scripts | | Controls user interface | +|:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface `. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|openmw.camera | by player scripts | | Controls camera (not implemented) | +|:ref:`openmw.camera ` | by player scripts | | Controls camera. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ **openmw_aux** @@ -72,3 +76,12 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid |:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +**Interfaces of built-in scripts** + ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +| Interface | Can be used | Description | ++=========================================================+====================+===============================================================+ +|:ref:`Camera ` | by player scripts | | Allows to alter behavior of the built-in camera script | +| | | | without overriding the script completely. | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ + diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index bcbee4349e..4f5f99965a 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -6,14 +6,22 @@ Engine handler is a function defined by a script, that can be called by the engi +---------------------------------------------------------------------------------------------------------+ | **Can be defined by any script** | +----------------------------------+----------------------------------------------------------------------+ -| onUpdate(dt) | | Called every frame if game not paused. `dt` is the time | +| onInit(initData) | | Called once when the script is created (not loaded). `InitData can`| +| | | `be assigned to a script in openmw-cs (not yet implemented)`. | +| | | ``onInterfaceOverride`` can be called before ``onInit``. | ++----------------------------------+----------------------------------------------------------------------+ +| onUpdate(dt) | | Called every frame if the game is not paused. `dt` is the time | | | | from the last update in seconds. | +----------------------------------+----------------------------------------------------------------------+ -| onSave() -> data | | Called when the game is saving. May be called in inactive | +| onSave() -> savedData | | Called when the game is saving. May be called in inactive | | | | state, so it shouldn't use `openmw.nearby`. | +----------------------------------+----------------------------------------------------------------------+ -| onLoad(data) | | Called on loading with the data previosly returned by | -| | | onSave. During loading the object is always inactive. | +| onLoad(savedData, initData) | | Called on loading with the data previosly returned by | +| | | onSave. During loading the object is always inactive. initData is | +| | | the same as in onInit. | ++----------------------------------+----------------------------------------------------------------------+ +| onInterfaceOverride(base) | | Called if the current script has an interface and overrides an | +| | | interface (``base``) of another script. | +----------------------------------+----------------------------------------------------------------------+ | **Only for global scripts** | +----------------------------------+----------------------------------------------------------------------+ @@ -36,6 +44,9 @@ Engine handler is a function defined by a script, that can be called by the engi +----------------------------------+----------------------------------------------------------------------+ | **Only for local scripts attached to a player** | +----------------------------------+----------------------------------------------------------------------+ +| onInputUpdate(dt) | | Called every frame (if the game is not paused) right after | +| | | processing user input. Use it only for latency-critical stuff. | ++----------------------------------+----------------------------------------------------------------------+ | onKeyPress(key) | | `Key `_ is pressed. | | | | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` | +----------------------------------+----------------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/interface_camera.rst b/docs/source/reference/lua-scripting/interface_camera.rst new file mode 100644 index 0000000000..c2db83b721 --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_camera.rst @@ -0,0 +1,6 @@ +Interface Camera +================ + +.. raw:: html + :file: generated_html/scripts_omw_camera.html + diff --git a/docs/source/reference/lua-scripting/openmw_camera.rst b/docs/source/reference/lua-scripting/openmw_camera.rst new file mode 100644 index 0000000000..4090843920 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_camera.rst @@ -0,0 +1,5 @@ +Package openmw.camera +===================== + +.. raw:: html + :file: generated_html/openmw_camera.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 44c79c35d8..938611635a 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -73,7 +73,7 @@ Let's write a simple example of a `Player script`: .. code-block:: Lua - -- Saved to my_lua_mod/example/player.lua + -- Save to my_lua_mod/example/player.lua local ui = require('openmw.ui') @@ -87,42 +87,82 @@ Let's write a simple example of a `Player script`: } } -In order to attach it to the player we also need a global script: +The script will be used only if it is specified in one of content files. +OpenMW Lua is an inclusive OpenMW feature, so it can not be controlled by ESP/ESM. +The options are: -.. code-block:: Lua - -- Saved to my_lua_mod/example/global.lua +1. Create text file "my_lua_mod.omwscripts" with the following line: +:: - return { - engineHandlers = { - onPlayerAdded = function(player) player:addScript('example/player.lua') end - } - } + PLAYER: example/player.lua -And one more file -- to start the global script: +2. (not implemented yet) Add the script in OpenMW CS on "Lua scripts" view and save as "my_lua_mod.omwaddon". + + +Enable it in ``openmw.cfg`` the same way as any other mod: :: - # Saved to my_lua_mod/my_lua_mod.omwscripts + data=path/to/my_lua_mod + content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon + +Now every time the player presses "X" on a keyboard, a message is shown. - # It is just a list of global scripts to run. Each file is on a separate line. - example/global.lua -Finally :ref:`register ` it in ``openmw.cfg``: +Format of ``.omwscripts`` +========================= :: - data=path/to/my_lua_mod - lua-scripts=my_lua_mod.omwscripts + # Lines starting with '#' are comments -Now every time the player presses "X" on a keyboard, a message is shown. + GLOBAL: my_mod/some_global_script.lua + + # Script that will be automatically attached to the player + PLAYER: my_mod/player.lua + + # Local script that will be automatically attached to every NPC and every creature in the game + NPC, CREATURE: my_mod/some_other_script.lua + + # Local script that can be attached to any object by a global script + CUSTOM: my_mod/something.lua + + # Local script that will be automatically attached to any Container AND can be + # attached to any other object by a global script. + CONTAINER, CUSTOM: my_mod/container.lua + +Each script is described by one line: +``: ``. +The order of lines determines the script load order (i.e. script priorities). + +Possible flags are: + +- ``GLOBAL`` - a global script; always active, can not by stopped; +- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script; +- ``PLAYER`` - an auto started player script; +- ``ACTIVATOR`` - a local script that will be automatically attached to any activator; +- ``ARMOR`` - a local script that will be automatically attached to any armor; +- ``BOOK`` - a local script that will be automatically attached to any book; +- ``CLOTHING`` - a local script that will be automatically attached to any clothing; +- ``CONTAINER`` - a local script that will be automatically attached to any container; +- ``CREATURE`` - a local script that will be automatically attached to any creature; +- ``DOOR`` - a local script that will be automatically attached to any door; +- ``INGREDIENT`` - a local script that will be automatically attached to any ingredient; +- ``LIGHT`` - a local script that will be automatically attached to any light; +- ``MISC_ITEM`` - a local script that will be automatically attached to any miscellaneous item; +- ``NPC`` - a local script that will be automatically attached to any NPC; +- ``POTION`` - a local script that will be automatically attached to any potion; +- ``WEAPON`` - a local script that will be automatically attached to any weapon. + +Several flags (except ``GLOBAL``) can be used with a single script. Use space or comma as a separator. Hot reloading ============= It is possible to modify a script without restarting OpenMW. To apply changes, open the in-game console and run the command: ``reloadlua``. This will restart all Lua scripts using the `onSave and onLoad`_ handlers the same way as if the game was saved or loaded. -It works only with existing ``*.lua`` files that are not packed to any archives. Adding new scripts or modifying ``*.omwscripts`` files always requires restarting the game. +It reloads all ``.omwscripts`` files and ``.lua`` files that are not packed to any archives. ``.omwaddon`` files and scripts packed to BSA can not be changed without restarting the game. Script structure ================ @@ -196,7 +236,7 @@ Engine handlers An engine handler is a function defined by a script, that can be called by the engine. I.e. it is an engine-to-script interaction. Not visible to other scripts. If several scripts register an engine handler with the same name, -the engine calls all of them in the same order as the scripts were started. +the engine calls all of them according to the load order (i.e. the order of ``content=`` entries in ``openmw.cfg``) and the order of scripts in ``omwaddon/omwscripts``. Some engine handlers are allowed only for global, or only for local/player scripts. Some are universal. See :ref:`Engine handlers reference`. @@ -210,12 +250,6 @@ The value that `onSave` returns will be passed to `onLoad` when the game is load It is the only way to save the internal state of a script. All other script variables will be lost after closing the game. The saved state must be :ref:`serializable `. -The list of active global scripts is controlled by ``*.omwscripts`` files. Loading a save doesn't synchronize -the list of global scripts with those that were active previously, it only calls `onLoad` for those currently active. - -For local scripts the situation is different. When a save is loading, it tries to run all local scripts that were saved. -So if ``lua-scripts=`` entries of some mod are removed, but ``data=`` entries are still enabled, then local scripts from the mod may still run. - `onSave` and `onLoad` can be called even for objects in inactive state, so it shouldn't use `openmw.nearby`. An example: @@ -316,9 +350,9 @@ Player scripts are local scripts that are attached to a player. +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.input ` | by player scripts | | User input | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.ui ` | by player scripts | | Controls user interface | +|:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface ` | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|openmw.camera | by player scripts | | Controls camera (not implemented) | +|:ref:`openmw.camera ` | by player scripts | | Controls camera | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ openmw_aux @@ -366,26 +400,28 @@ Overriding the interface and adding a debug output: .. code-block:: Lua - local interfaces = require('openmw.interfaces') - - -- it is important to save it before returning the new interface - local orig = interfaces.SomeUtils + local baseInterface = nil -- will be assigned by `onInterfaceOverride` + interface = { + version = 1, + doSomething = function(x, y) + print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) + baseInterface.doSomething(x, y) -- calls the original `doSomething` + + -- WRONG! Would lead to an infinite recursion. + -- local interfaces = require('openmw.interfaces') + -- interfaces.SomeUtils.doSomething(x, y) + end, + } return { - interfaceName = "SomeUtils" - interface = { - version = orig.version, - doSomething = function(x, y) - print(string.format('SomeUtils.doSomething(%d, %d)', x, y)) - orig.doSomething(x, y) -- calls the original `doSomething` - - -- WRONG! Would lead to an infinite recursion. - -- interfaces.SomeUtils.doSomething(x, y) - end, - } + interfaceName = "SomeUtils", + interface = interface, + engineHandlers = { + onInterfaceOverride = function(base) baseInterface = base end, + }, } -A general recomendation about overriding is that the new interface should be fully compatible with the old one. +A general recommendation about overriding is that the new interface should be fully compatible with the old one. So it is fine to change the behaviour of `SomeUtils.doSomething`, but if you want to add a completely new function, it would be better to create a new interface for it. For example `SomeUtilsExtended` with an additional function `doSomethingElse`. @@ -403,6 +439,15 @@ Using the interface: The order in which the scripts are started is important. So if one mod should override an interface provided by another mod, make sure that load order (i.e. the sequence of `lua-scripts=...` in `openmw.cfg`) is correct. +**Interfaces of built-in scripts** + ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +| Interface | Can be used | Description | ++=========================================================+====================+===============================================================+ +|:ref:`Camera ` | by player scripts | | Allows to alter behavior of the built-in camera script | +| | | | without overriding the script completely. | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ + Event system ============ @@ -418,7 +463,7 @@ Events are the main way of interacting between local and global scripts. They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient. If several scripts register handlers for the same event, the handlers will be called in reverse order (opposite to engine handlers). -I.e. the handler from the last attached script will be called first. +I.e. the handler from the last script in the load order will be called first. Return value 'false' means "skip all other handlers for this event". Any other return value (including nil) means nothing. @@ -471,7 +516,7 @@ The protection mod attaches an additional local script to every actor. The scrip eventHandlers = { DamagedByDarkPower = reduceDarkDamage }, } -In order to be able to intercept the event, the protection script should be attached after the original script (i.e. below in the load order). +In order to be able to intercept the event, the protection script should be placed in the load order below the original script. Timers @@ -683,8 +728,7 @@ you can import these files to get code autocompletion and integrated OpenMW API .. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-project-settings.png - Press `Next`, choose the `Libraries` tab, and click `Add External Source Folder`. -- Specify there the path to ``resources/lua_api`` in your OpenMW installation. -- If you use `openmw_aux`_, add ``resources/vfs`` as an additional external source folder. +- Specify there paths to ``resources/lua_api`` and ``resources/vfs`` in your OpenMW installation. .. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-import-api.png @@ -713,6 +757,17 @@ You can add special hints to give LDT more information: .. image:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/reference/lua-scripting/_static/lua-ide-code-completion2.png -See `LDT Documentation Language `__ for more details. +In order to have autocompletion for script interfaces the information where to find these interfaces should be provided. +For example for the camera interface (defined in ``resources/vfs/scripts/omw/camera.lua``): + +.. code-block:: Lua + --- @type Interfaces + -- @field scripts.omw.camera#Interface Camera + -- ... other interfaces here + --- @field #Interfaces I + local I = require('openmw.interfaces') + I.Camera.disableZoom() + +See `LDT Documentation Language `__ for more details. diff --git a/docs/source/reference/lua-scripting/user_interface.rst b/docs/source/reference/lua-scripting/user_interface.rst new file mode 100644 index 0000000000..88233dceb9 --- /dev/null +++ b/docs/source/reference/lua-scripting/user_interface.rst @@ -0,0 +1,159 @@ +User interface reference +======================== + +.. toctree:: + :hidden: + + widgets/widget + +Layouts +------- + +Every widget is defined by a layout, which is a Lua table with the following fields (all of them are optional): + +1. `type`: One of the available widget types from `openmw.ui.TYPE`. +2. | `props`: A Lua table, containing all the properties values. + | Properties define most of the information about the widget: its position, data it displays, etc. + | See the widget pages (table below) for details on specific properties. + | Properties of the basic Widget are inherited by all the other widgets. +3. | `events`: A Lua table, containing `openmw.async.callback` values, which trigger on various interactions with the widget. + | See the Widget pages for details on specific events. + | Events of the basic Widget are inherited by all the other widgets. +4. `content`: a Content (`openmw.ui.content`), which contains layouts for the children of this widget. +5. | `name`: an arbitrary string, the only limitatiion is it being unique within a `Content`. + | Helpful for navigatilng through the layouts. +6. `layer`: only applies for the root widget. (Windows, HUD, etc) + +.. TODO: Write a more detailed documentation for layers when they are finished + +Elements +-------- + +Element is the root widget of a layout. +It is an independent part of the UI, connected only to a specific layer, but not any other layouts. +Creating or destroying an element also creates/destroys all of its children. + +Content +------- + +A container holding all the widget's children. It has a few important differences from a Lua table: + +1. All the keys are integers, i. e. it is an "array" +2. Holes are not allowed. At any point all keys from `1` to the highest `n` must contain a value. +3. | You can access the values by their `name` field as a `Content` key. + | While there is nothing preventing you from changing the `name` of a table inside a content, it is not supported, and will lead to undefined behaviour. + | If you have to change the name, assign a new table to the index instead. + +.. TODO: Talk about skins/templates here when they are ready + +Events +------ + +| A table mapping event names to `openmw.async.callback` s. +| When an event triggers, the callback is called with two arguments: + an event-specific value, and that widget's layout table. +| See the Widget type pages for information on what events exist, and which first argument they pass. + +Widget types +------------ + +.. list-table:: + :widths: 30 70 + + * - :ref:`Widget` + - Base widget type, all the other widget inherit its properties and events. + * - `Text` + - Displays text. + * - EditText + - Accepts text input from the user. + * - Window + - Can be moved and resized by the user. + +Example +------- + +*scripts/requirePassword.lua* + +.. code-block:: Lua + + local core = require('openmw.core') + local async = require('openmw.async') + local ui = require('openmw.ui') + local v2 = require('openmw.util').vector2 + + local layout = { + layers = 'Windows', + type = ui.TYPE.Window, + skin = 'MW_Window', -- TODO: replace all skins here when they are properly implemented + props = { + size = v2(200, 250), + -- put the window in the middle of the screen + relativePosition = v2(0.5, 0.5), + anchor = v2(0.5, 0.5), + }, + content = ui.content { + { + type = ui.TYPE.Text, + skin = 'SandText', + props = { + caption = 'Input password', + relativePosition = v2(0.5, 0), + anchor = v2(0.5, 0), + }, + }, + { + name = 'input', + type = ui.TYPE.TextEdit, + skin = "MW_TextEdit", + props = { + caption = '', + relativePosition = v2(0.5, 0.5), + anchor = v2(0.5, 0.5), + size = v2(125, 50), + }, + events = {} + }, + { + name = 'submit', + type = ui.TYPE.Text, -- TODO: replace with button when implemented + skin = "MW_Button", + props = { + caption = 'Submit', + -- position at the bottom + relativePosition = v2(0.5, 1.0), + anchor = v2(0.5, 1.0), + autoSize = false, + size = v2(75, 50), + }, + events = {}, + }, + }, + } + + local element = nil + + local input = layout.content.input + -- TODO: replace with a better event when TextEdit is finished + input.events.textInput = async:callback(function(text) + input.props.caption = input.props.caption .. text + end) + + local submit = layout.content.submit + submit.events.mouseClick = async:callback(function() + if input.props.caption == 'very secret password' then + if element then + element:destroy() + end + else + print('wrong password', input.props.caption) + core.quit() + end + end) + + element = ui.create(layout) + +*requirePassword.omwscripts* + +:: + + PLAYER: scripts/requirePassword.lua diff --git a/docs/source/reference/lua-scripting/widgets/widget.rst b/docs/source/reference/lua-scripting/widgets/widget.rst new file mode 100644 index 0000000000..49058ee278 --- /dev/null +++ b/docs/source/reference/lua-scripting/widgets/widget.rst @@ -0,0 +1,77 @@ +Widget +====== + +Properties +---------- + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type (default value) + - description + * - position + - util.vector2 (0, 0) + - | Offsets the position of the widget from its parent's + | top-left corner in pixels. + * - size + - util.vector2 (0, 0) + - Increases the widget's size in pixels. + * - relativePosition + - util.vector2 (0, 0) + - | Offsets the position of the widget from its parent's + | top-left corner as a fraction of the parent's size. + * - relativeSize + - util.vector2 (0, 0) + - Increases the widget's size by a fraction of its parent's size. + * - anchor + - util.vector2 (0, 0) + - | Offsets the widget's position by a fraction of its size. + | Useful for centering or aligning to a corner. + * - visible + - boolean (true) + - Defines if the widget is visible + +Events +------ + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - first argument type + - description + * - keyPress + - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_ + - A key was pressed with this widget in focus + * - keyRelease + - `KeyboardEvent <../openmw_input.html##(KeyboardEvent)>`_ + - A key was released with this widget in focus + * - mouseMove + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - | Mouse cursor moved on this widget + | `MouseEvent.button` is the mouse button being held + | (nil when simply moving, and not dragging) + * - mouseClick + - nil + - Widget was clicked with left mouse button + * - mouseDoubleClick + - nil + - Widget was double clicked with left mouse button + * - mousePress + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - A mouse button was pressed on this widget + * - mouseRelease + - `MouseEvent <../openmw_ui.html##(MouseEvent)>`_ + - A mouse button was released on this widget + * - focusGain + - nil + - Widget gained focus (either through mouse or keyboard) + * - focusLoss + - nil + - Widget lost focus + * - textInput + - string + - Text input with this widget in focus diff --git a/docs/source/reference/modding/extended.rst b/docs/source/reference/modding/extended.rst index f107617b33..db7df2e916 100644 --- a/docs/source/reference/modding/extended.rst +++ b/docs/source/reference/modding/extended.rst @@ -333,17 +333,9 @@ Lua scripting OpenMW supports Lua scripts. See :ref:`Lua scripting documentation `. It is not compatible with MWSE. A mod with Lua scripts will work only if it was developed specifically for OpenMW. -Mods can contain ``*.omwscripts`` files. They should be registered in the ``openmw.cfg`` via "lua-scripts" entries. The order of the "lua-scripts" entries can be important. If "some_lua_mod" uses API provided by "another_lua_mod", then omwscripts from "another_lua_mod" should be registered first. For example: - -:: - - data="path/to/another_lua_mod" - content=another_lua_mod.omwaddon - lua-scripts=another_lua_mod.omwscripts - - data="path/to/some_lua_mod" - content=some_lua_mod.omwaddon - lua-scripts=some_lua_mod.omwscripts +Installation of a Lua mod is the same as of any other mod: add ``data=`` and ``content=`` entries to ``openmw.cfg``. +Files with suffix ``.omwscripts`` are special type of content files and should also be enabled using ``content=`` entries. +Note that for some mods load order can be important. .. _`Graphic Herbalism`: https://www.nexusmods.com/morrowind/mods/46599 .. _`OpenMW Containers Animated`: https://www.nexusmods.com/morrowind/mods/46232 diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 878485b3b3..605be3f6af 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -275,7 +275,7 @@ normalise race speed :Range: True/False :Default: False -By default race weight is factored into horizontal movement speed like in Morrowind. +By default race weight is factored into horizontal movement and magic projectile speed like in Morrowind. For example, an NPC which has 1.2 race weight is faster than an NPC with the exact same stats and weight 1.0 by a factor of 120%. If this setting is true, race weight is ignored in the calculations which allows for a movement behavior equivalent to the one introduced by the equivalent Morrowind Code Patch feature. @@ -441,7 +441,7 @@ Some mods add harvestable container models. When this setting is enabled, activa 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 @@ -455,3 +455,12 @@ If disabled actors without the ability to swim will not follow other actors to t Has effect only when Navigator is enabled. This setting can be controlled in Advanced tab of the launcher. + +default actor pathfind half extents +----------------------------------- + +:Type: 3D vector floating point +:Range: All components > 0 +:Default: 29.27999496459961 28.479997634887695 66.5 + +Actor half extents used for exterior cells to generate navmesh. diff --git a/docs/source/reference/modding/settings/general.rst b/docs/source/reference/modding/settings/general.rst index f0ebe4f972..ee5b908b4a 100644 --- a/docs/source/reference/modding/settings/general.rst +++ b/docs/source/reference/modding/settings/general.rst @@ -61,7 +61,7 @@ Mipmapping is a way of reducing the processing power needed during minification by pregenerating a series of smaller textures. notify on saved screenshot --------------- +-------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index 3e943e4284..7b060f58ad 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -51,6 +51,7 @@ Determines whether grass should respond to the player treading on it. .. list-table:: Modes :header-rows: 1 + * - Mode number - Meaning * - 0 @@ -77,6 +78,7 @@ How far away from the player grass can be before it's unaffected by being trod o .. list-table:: Presets :header-rows: 1 + * - Preset number - Range (Units) - Distance (Units) diff --git a/docs/source/reference/modding/settings/lua.rst b/docs/source/reference/modding/settings/lua.rst index 4d1a3ca808..65faf884ae 100644 --- a/docs/source/reference/modding/settings/lua.rst +++ b/docs/source/reference/modding/settings/lua.rst @@ -1,6 +1,18 @@ Lua Settings ############ +lua debug +--------- + +:Type: boolean +:Range: True/False +:Default: False + +Enables debug tracebacks for Lua actions. +It adds significant performance overhead, don't enable if you don't need it. + +This setting can only be configured by editing the settings configuration file. + lua num threads --------------- diff --git a/docs/source/reference/modding/settings/map.rst b/docs/source/reference/modding/settings/map.rst index a4d3cd7e0d..1412d6584f 100644 --- a/docs/source/reference/modding/settings/map.rst +++ b/docs/source/reference/modding/settings/map.rst @@ -125,6 +125,7 @@ max local viewing distance This setting controls the viewing distance on local map when 'distant terrain' is enabled. If this setting is greater than the viewing distance then only up to the viewing distance is used for local map, otherwise the viewing distance is used. If view distance is changed in settings menu during the game, then viewable distance on the local map is not updated. + .. warning:: Increasing this setting can increase cell load times, because the localmap take a snapshot of each cell contained in a square of 2 x (max local viewing distance) + 1 square. diff --git a/docs/source/reference/modding/settings/navigator.rst b/docs/source/reference/modding/settings/navigator.rst index fee4b2626e..aea817530e 100644 --- a/docs/source/reference/modding/settings/navigator.rst +++ b/docs/source/reference/modding/settings/navigator.rst @@ -1,5 +1,5 @@ Navigator Settings -################ +################## Main settings ************* @@ -43,7 +43,7 @@ Increasing this value may decrease performance. It's a limitation of `Recastnavigation `_ library. wait until min distance to player ------------------------------- +--------------------------------- :Type: integer :Range: >= 0 @@ -87,7 +87,7 @@ Memory will be consumed in approximately linear dependency from number of nav me But only for new locations or already dropped from cache. min update interval ms ----------------- +---------------------- :Type: integer :Range: >= 0 @@ -181,7 +181,7 @@ Every nav mesh is visible and every update is noticable. Potentially decreases performance. enable agents paths render -------------------- +-------------------------- :Type: boolean :Range: True/False @@ -193,7 +193,7 @@ Works even if Navigator is disabled. Potentially decreases performance. enable recast mesh render ----------------------- +------------------------- :Type: boolean :Range: True/False diff --git a/docs/source/reference/modding/settings/shaders.rst b/docs/source/reference/modding/settings/shaders.rst index 03b7805de6..b9d8cfe1b9 100644 --- a/docs/source/reference/modding/settings/shaders.rst +++ b/docs/source/reference/modding/settings/shaders.rst @@ -241,7 +241,7 @@ lighting` is on. This setting has no effect if :ref:`lighting method` is 'legacy'. minimum interior brightness ------------------------- +--------------------------- :Type: float :Range: 0.0-1.0 @@ -260,7 +260,7 @@ aforementioned changes in visuals. This setting has no effect if :ref:`lighting method` is 'legacy'. antialias alpha test ---------------------------------------- +-------------------- :Type: boolean :Range: True/False @@ -269,3 +269,16 @@ antialias alpha test Convert the alpha test (cutout/punchthrough alpha) to alpha-to-coverage when :ref:`antialiasing` is on. 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. + +soft particles +-------------- + +:Type: boolean +:Range: True/False +:Default: False + +Enables soft particles for particle effects. This technique softens the +intersection between individual particles and other opaque geometry by blending +between them. Note, this relies on overriding specific properties of particle +systems that potentially differ from the source content, this setting may change +the look of some particle systems. diff --git a/docs/source/reference/modding/settings/water.rst b/docs/source/reference/modding/settings/water.rst index b3a6f8c1f2..fb3b755c6f 100644 --- a/docs/source/reference/modding/settings/water.rst +++ b/docs/source/reference/modding/settings/water.rst @@ -77,6 +77,21 @@ Controls what kinds of things are rendered in water reflections. In interiors the lowest level is 2. This setting can be changed ingame with the "Reflection shader detail" dropdown under the Water tab of the Video panel in the Options menu. +rain ripple detail +----------------- + +:Type: integer +:Range: 0, 1, 2 +:Default: 2 + +Controls how detailed the raindrop ripples on water are. + +0: single, non-normal-mapped ring per raindrop +1: normal-mapped raindrops, with multiple rings +2: same as 1, but with a greater number of raindrops + +This setting can be changed ingame with the "Rain ripple detail/density" dropdown under the Water tab of the Video panel in the Options menu. + small feature culling pixel size -------------------------------- diff --git a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst index 0ad35d7a50..e05177c268 100644 --- a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst +++ b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst @@ -15,7 +15,7 @@ Normal maps from Morrowind to OpenMW - `Tutorial - Morrowind, Part 2`_ General introduction to normal map conversion ------------------------------------------------- +--------------------------------------------- :Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov :Updated: 2020-03-03 @@ -34,7 +34,7 @@ There are several techniques for bump-mapping, and normal-mapping is the most co So let's get on with it. OpenMW normal-mapping -************************ +********************* Normal-mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix, and you're done. @@ -70,7 +70,7 @@ settings.cfg_-file. Add these rows where it would make sense: See OpenMW's wiki page about `texture modding`_ to read more about it. Morrowind bump-mapping -***************************************************** +********************** **Conversion difficulty:** *Varies. Sometimes quick and easy, sometimes time-consuming and hard.* @@ -93,7 +93,7 @@ In this case you can benefit from OpenMW's normal-mapping support by using these This means that you will have to drop the bump-mapping references from the model and sometimes rename the texture. MGE XE normal-mapping -*************************************** +********************* **Conversion difficulty:** *Easy* @@ -169,7 +169,7 @@ depending on a few circumstances. In this tutorial, we will look at a very easy, although in some cases a bit time-consuming, example. Tutorial - Morrowind, Part 1 -********************** +**************************** We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_. Since this is just a texture pack and not a model replacer, @@ -201,7 +201,7 @@ We ignored those model files since they are not needed with OpenMW. In this tuto we will convert a mod that includes new, custom-made models. In other words, we cannot just ignore those files this time. Tutorial - Morrowind, Part 2 -********************** +**************************** The sacks included in Apel's `Various Things - Sacks`_ come in two versions – without bump-mapping, and with bump-mapping. Since we want the glory of normal-mapping in our OpenMW setup, we will go with the bump-mapped version. diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 6ed0bffa6b..0bfdc4c233 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -188,3 +188,25 @@ if(NOT OPENMW_USE_SYSTEM_RECASTNAVIGATION) ) FetchContent_MakeAvailableExcludeFromAll(recastnavigation) endif() + +if (NOT OPENMW_USE_SYSTEM_SQLITE3) + include(FetchContent) + FetchContent_Declare(sqlite3 + URL https://www.sqlite.org/2021/sqlite-amalgamation-3360000.zip + URL_HASH MD5=c5d360c74111bafae1b704721ff18fe6 + SOURCE_DIR fetched/sqlite3 + ) + FetchContent_MakeAvailableExcludeFromAll(sqlite3) + + add_library(sqlite3 STATIC ${sqlite3_SOURCE_DIR}/sqlite3.c) + target_include_directories(sqlite3 INTERFACE ${sqlite3_SOURCE_DIR}/) + if (UNIX) + target_link_libraries(sqlite3 ${CMAKE_DL_LIBS}) + endif() + add_library(SQLite::SQLite3 ALIAS sqlite3) + + set(SQLite3_INCLUDE_DIR ${sqlite3_SOURCE_DIR}/ PARENT_SCOPE) + set(SQLite3_LIBRARY sqlite3 PARENT_SCOPE) +endif() + +add_subdirectory(smhasher) diff --git a/extern/smhasher/CMakeLists.txt b/extern/smhasher/CMakeLists.txt new file mode 100644 index 0000000000..ee03e6c38e --- /dev/null +++ b/extern/smhasher/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(smhasher STATIC MurmurHash3.cpp) +target_include_directories(smhasher INTERFACE .) diff --git a/extern/smhasher/MurmurHash3.cpp b/extern/smhasher/MurmurHash3.cpp new file mode 100644 index 0000000000..c8b774bab9 --- /dev/null +++ b/extern/smhasher/MurmurHash3.cpp @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +// Note - The x86 and x64 versions do _not_ produce the same results, as the +// algorithms are optimized for their respective platforms. You can still +// compile and run any of them on any platform, but your performance with the +// non-native version will be less than optimal. + +#include "MurmurHash3.h" + +#include + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) + +#define FORCE_INLINE __forceinline + +#include + +#define ROTL64(x,y) _rotl64(x,y) + +#define BIG_CONSTANT(x) (x) + +// Other compilers + +#else // defined(_MSC_VER) + +#define FORCE_INLINE inline __attribute__((always_inline)) + +inline uint64_t rotl64 ( uint64_t x, int8_t r ) +{ + return (x << r) | (x >> (64 - r)); +} + +#define ROTL64(x,y) rotl64(x,y) + +#define BIG_CONSTANT(x) (x##LLU) + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- +// Block read - if your platform needs to do endian-swapping or can only +// handle aligned reads, do the conversion here + +FORCE_INLINE uint64_t getblock64 ( const uint64_t * p, int i ) +{ + uint64_t result = 0; + std::memcpy(&result, p + i, sizeof(result)); + return result; +} + +//---------- + +FORCE_INLINE uint64_t fmix64 ( uint64_t k ) +{ + k ^= k >> 33; + k *= BIG_CONSTANT(0xff51afd7ed558ccd); + k ^= k >> 33; + k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); + k ^= k >> 33; + + return k; +} + +//----------------------------------------------------------------------------- + +void MurmurHash3_x64_128 ( const void * key, const int len, + const uint64_t * seed, void * out ) +{ + const uint8_t * data = (const uint8_t*)key; + const int nblocks = len / 16; + + uint64_t h1 = seed[0]; + uint64_t h2 = seed[1]; + + const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); + const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); + + //---------- + // body + + const uint64_t * blocks = (const uint64_t *)(data); + + for(int i = 0; i < nblocks; i++) + { + uint64_t k1 = getblock64(blocks,i*2+0); + uint64_t k2 = getblock64(blocks,i*2+1); + + k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; + + h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729; + + k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; + + h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5; + } + + //---------- + // tail + + const uint8_t * tail = (const uint8_t*)(data + nblocks*16); + + uint64_t k1 = 0; + uint64_t k2 = 0; + + switch(len & 15) + { + case 15: k2 ^= ((uint64_t)tail[14]) << 48; + case 14: k2 ^= ((uint64_t)tail[13]) << 40; + case 13: k2 ^= ((uint64_t)tail[12]) << 32; + case 12: k2 ^= ((uint64_t)tail[11]) << 24; + case 11: k2 ^= ((uint64_t)tail[10]) << 16; + case 10: k2 ^= ((uint64_t)tail[ 9]) << 8; + case 9: k2 ^= ((uint64_t)tail[ 8]) << 0; + k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; + + case 8: k1 ^= ((uint64_t)tail[ 7]) << 56; + case 7: k1 ^= ((uint64_t)tail[ 6]) << 48; + case 6: k1 ^= ((uint64_t)tail[ 5]) << 40; + case 5: k1 ^= ((uint64_t)tail[ 4]) << 32; + case 4: k1 ^= ((uint64_t)tail[ 3]) << 24; + case 3: k1 ^= ((uint64_t)tail[ 2]) << 16; + case 2: k1 ^= ((uint64_t)tail[ 1]) << 8; + case 1: k1 ^= ((uint64_t)tail[ 0]) << 0; + k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; h2 ^= len; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + ((uint64_t*)out)[0] = h1; + ((uint64_t*)out)[1] = h2; +} + +//----------------------------------------------------------------------------- + diff --git a/extern/smhasher/MurmurHash3.h b/extern/smhasher/MurmurHash3.h new file mode 100644 index 0000000000..8aebdc304d --- /dev/null +++ b/extern/smhasher/MurmurHash3.h @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +#ifndef _MURMURHASH3_H_ +#define _MURMURHASH3_H_ + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) && (_MSC_VER < 1600) + +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; + +// Other compilers + +#else // defined(_MSC_VER) + +#include + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +void MurmurHash3_x64_128 ( const void * key, int len, const uint64_t * seed, void * out ); + +//----------------------------------------------------------------------------- + +#endif // _MURMURHASH3_H_ diff --git a/files/builtin_scripts/CMakeLists.txt b/files/builtin_scripts/CMakeLists.txt index 5906680e1b..6f290cc1f7 100644 --- a/files/builtin_scripts/CMakeLists.txt +++ b/files/builtin_scripts/CMakeLists.txt @@ -1,8 +1,20 @@ -file(GLOB_RECURSE BUILTIN_SCRIPTS LIST_DIRECTORIES false RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} CONFIGURE_DEPENDS "*") +if (NOT DEFINED OPENMW_RESOURCES_ROOT) + return() +endif() -foreach (f ${BUILTIN_SCRIPTS}) - if (NOT ("CMakeLists.txt" STREQUAL "${f}")) - copy_resource_file("${CMAKE_CURRENT_SOURCE_DIR}/${f}" "${OpenMW_BINARY_DIR}" "resources/vfs/${f}") - endif() -endforeach (f) +# Copy resource files into the build directory +set(SDIR ${CMAKE_CURRENT_SOURCE_DIR}) +set(DDIRRELATIVE resources/vfs) +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "builtin.omwscripts") +set(DDIRRELATIVE resources/vfs/openmw_aux) +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "openmw_aux/util.lua") + +set(LUA_SCRIPTS_FILES + scripts/omw/camera.lua + scripts/omw/head_bobbing.lua + scripts/omw/third_person.lua +) + +set(DDIRRELATIVE resources/vfs/scripts/omw) +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${LUA_SCRIPTS_FILES}") diff --git a/files/builtin_scripts/builtin.omwscripts b/files/builtin_scripts/builtin.omwscripts new file mode 100644 index 0000000000..30fccad9fa --- /dev/null +++ b/files/builtin_scripts/builtin.omwscripts @@ -0,0 +1 @@ +PLAYER: scripts/omw/camera.lua diff --git a/files/builtin_scripts/scripts/omw/camera.lua b/files/builtin_scripts/scripts/omw/camera.lua new file mode 100644 index 0000000000..1a5d1ca730 --- /dev/null +++ b/files/builtin_scripts/scripts/omw/camera.lua @@ -0,0 +1,243 @@ +local camera = require('openmw.camera') +local input = require('openmw.input') +local settings = require('openmw.settings') +local util = require('openmw.util') +local self = require('openmw.self') + +local head_bobbing = require('scripts.omw.head_bobbing') +local third_person = require('scripts.omw.third_person') + +local MODE = camera.MODE + +local previewIfStandSill = settings._getBoolFromSettingsCfg('Camera', 'preview if stand still') +local showCrosshairInThirdPerson = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder') + +local primaryMode + +local noModeControl = 0 +local noStandingPreview = 0 +local noHeadBobbing = 0 +local noZoom = 0 + +local function init() + camera.allowCharacterDeferredRotation(settings._getBoolFromSettingsCfg('Camera', 'deferred preview rotation')) + if camera.getMode() == MODE.FirstPerson then + primaryMode = MODE.FirstPerson + else + primaryMode = MODE.ThirdPerson + camera.setMode(MODE.ThirdPerson) + end +end + +local smoothedSpeed = 0 +local previewTimer = 0 + +local function updatePOV(dt) + local switchLimit = 0.25 + if input.isActionPressed(input.ACTION.TogglePOV) and input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) then + previewTimer = previewTimer + dt + if primaryMode == MODE.ThirdPerson or previewTimer >= switchLimit then + third_person.standingPreview = false + camera.setMode(MODE.Preview) + end + elseif previewTimer > 0 then + if previewTimer <= switchLimit then + if primaryMode == MODE.FirstPerson then + primaryMode = MODE.ThirdPerson + else + primaryMode = MODE.FirstPerson + end + end + camera.setMode(primaryMode) + previewTimer = 0 + end +end + +local idleTimer = 0 +local vanityDelay = settings.getGMST('fVanityDelay') + +local function updateVanity(dt) + if input.isIdle() then + idleTimer = idleTimer + dt + else + idleTimer = 0 + end + local vanityAllowed = input.getControlSwitch(input.CONTROL_SWITCH.VanityMode) + if vanityAllowed and idleTimer > vanityDelay and camera.getMode() ~= MODE.Vanity then + camera.setMode(MODE.Vanity) + end + if camera.getMode() == MODE.Vanity then + if not vanityAllowed or idleTimer == 0 then + camera.setMode(primaryMode) + else + camera.setYaw(camera.getYaw() + math.rad(3) * dt) + end + end +end + +local function updateSmoothedSpeed(dt) + local speed = self:getCurrentSpeed() + speed = speed / (1 + speed / 500) + local maxDelta = 300 * dt + smoothedSpeed = smoothedSpeed + util.clamp(speed - smoothedSpeed, -maxDelta, maxDelta) +end + +local minDistance = 30 +local maxDistance = 800 + +local function zoom(delta) + if not input.getControlSwitch(input.CONTROL_SWITCH.ViewMode) or + not input.getControlSwitch(input.CONTROL_SWITCH.Controls) or + camera.getMode() == MODE.Static or noZoom > 0 then + return + end + if camera.getMode() ~= MODE.FirstPerson then + local obstacleDelta = third_person.preferredDistance - camera.getThirdPersonDistance() + if delta > 0 and third_person.baseDistance == minDistance and + (camera.getMode() ~= MODE.Preview or third_person.standingPreview) and noModeControl == 0 then + primaryMode = MODE.FirstPerson + camera.setMode(primaryMode) + elseif delta > 0 or obstacleDelta < -delta then + third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, maxDistance) + end + elseif delta < 0 and noModeControl == 0 then + primaryMode = MODE.ThirdPerson + camera.setMode(primaryMode) + third_person.baseDistance = minDistance + end +end + +local function applyControllerZoom(dt) + if camera.getMode() == MODE.Preview then + local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft) + local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) + local controllerZoom = (triggerRight - triggerLeft) * 100 * dt + if controllerZoom ~= 0 then + zoom(controllerZoom) + end + end +end + +local function updateStandingPreview() + local mode = camera.getMode() + if not previewIfStandSill or noStandingPreview > 0 + or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then + third_person.standingPreview = false + return + end + local standingStill = self:getCurrentSpeed() == 0 and not self:isInWeaponStance() and not self:isInMagicStance() + if standingStill and mode == MODE.ThirdPerson then + third_person.standingPreview = true + camera.setMode(MODE.Preview) + elseif not standingStill and third_person.standingPreview then + third_person.standingPreview = false + camera.setMode(primaryMode) + end +end + +local function updateCrosshair() + camera.showCrosshair( + camera.getMode() == MODE.FirstPerson or + (showCrosshairInThirdPerson and (camera.getMode() == MODE.ThirdPerson or third_person.standingPreview))) +end + +local function onUpdate(dt) + camera.setExtraPitch(0) + camera.setExtraYaw(0) + camera.setRoll(0) + camera.setFirstPersonOffset(util.vector3(0, 0, 0)) + updateSmoothedSpeed(dt) +end + +local function onInputUpdate(dt) + local mode = camera.getMode() + if mode == MODE.FirstPerson or mode == MODE.ThirdPerson then + primaryMode = mode + end + if mode ~= MODE.Static then + if not camera.getQueuedMode() or camera.getQueuedMode() == MODE.Preview then + if noModeControl == 0 then + updatePOV(dt) + updateVanity(dt) + end + updateStandingPreview() + end + updateCrosshair() + end + applyControllerZoom(dt) + third_person.update(dt, smoothedSpeed) + if noHeadBobbing == 0 then head_bobbing.update(dt, smoothedSpeed) end +end + +return { + interfaceName = 'Camera', + --- @module Camera + -- @usage require('openmw.interfaces').Camera + interface = { + --- @field [parent=#Camera] #number version Interface version + version = 0, + + --- @function [parent=#Camera] getPrimaryMode Returns primary mode (MODE.FirstPerson or MODE.ThirdPerson). + getPrimaryMode = function() return primaryMode end, + --- @function [parent=#Camera] getBaseThirdPersonDistance + getBaseThirdPersonDistance = function() return third_person.baseDistance end, + --- @function [parent=#Camera] setBaseThirdPersonDistance + setBaseThirdPersonDistance = function(v) third_person.baseDistance = v end, + --- @function [parent=#Camera] getTargetThirdPersonDistance + getTargetThirdPersonDistance = function() return third_person.preferredDistance end, + + --- @function [parent=#Camera] isModeControlEnabled + isModeControlEnabled = function() return noModeControl == 0 end, + --- @function [parent=#Camera] disableModeControl + disableModeControl = function() noModeControl = noModeControl + 1 end, + --- @function [parent=#Camera] enableModeControl + enableModeControl = function() noModeControl = math.max(0, noModeControl - 1) end, + + --- @function [parent=#Camera] isStandingPreviewEnabled + isStandingPreviewEnabled = function() return previewIfStandSill and noStandingPreview == 0 end, + --- @function [parent=#Camera] disableStandingPreview + disableStandingPreview = function() noStandingPreview = noStandingPreview + 1 end, + --- @function [parent=#Camera] enableStandingPreview + enableStandingPreview = function() noStandingPreview = math.max(0, noStandingPreview - 1) end, + + --- @function [parent=#Camera] isHeadBobbingEnabled + isHeadBobbingEnabled = function() return head_bobbing.enabled and noHeadBobbing == 0 end, + --- @function [parent=#Camera] disableHeadBobbing + disableHeadBobbing = function() noHeadBobbing = noHeadBobbing + 1 end, + --- @function [parent=#Camera] enableHeadBobbing + enableHeadBobbing = function() noHeadBobbing = math.max(0, noHeadBobbing - 1) end, + + --- @function [parent=#Camera] isZoomEnabled + isZoomEnabled = function() return noZoom == 0 end, + --- @function [parent=#Camera] disableZoom + disableZoom = function() noZoom = noZoom + 1 end, + --- @function [parent=#Camera] enableZoom + enableZoom = function() noZoom = math.max(0, noZoom - 1) end, + + --- @function [parent=#Camera] isThirdPersonOffsetControlEnabled + isThirdPersonOffsetControlEnabled = function() return third_person.noOffsetControl == 0 end, + --- @function [parent=#Camera] disableThirdPersonOffsetControl + disableThirdPersonOffsetControl = function() third_person.noOffsetControl = third_person.noOffsetControl + 1 end, + --- @function [parent=#Camera] enableThirdPersonOffsetControl + enableThirdPersonOffsetControl = function() third_person.noOffsetControl = math.max(0, third_person.noOffsetControl - 1) end, + }, + engineHandlers = { + onUpdate = onUpdate, + onInputUpdate = onInputUpdate, + onInputAction = function(action) + if action == input.ACTION.ZoomIn then + zoom(10) + elseif action == input.ACTION.ZoomOut then + zoom(-10) + end + end, + onActive = init, + onLoad = function(data) + if data and data.distance then third_person.baseDistance = data.distance end + end, + onSave = function() + return {version = 0, distance = third_person.baseDistance} + end, + }, +} + diff --git a/files/builtin_scripts/scripts/omw/head_bobbing.lua b/files/builtin_scripts/scripts/omw/head_bobbing.lua new file mode 100644 index 0000000000..fe809fca8a --- /dev/null +++ b/files/builtin_scripts/scripts/omw/head_bobbing.lua @@ -0,0 +1,51 @@ +local camera = require('openmw.camera') +local self = require('openmw.self') +local settings = require('openmw.settings') +local util = require('openmw.util') + +local doubleStepLength = settings._getFloatFromSettingsCfg('Camera', 'head bobbing step') * 2 +local stepHeight = settings._getFloatFromSettingsCfg('Camera', 'head bobbing height') +local maxRoll = math.rad(settings._getFloatFromSettingsCfg('Camera', 'head bobbing roll')) + +local effectWeight = 0 +local totalMovement = 0 + +local M = { + enabled = settings._getBoolFromSettingsCfg('Camera', 'head bobbing') +} + +-- Trajectory of each step is a scaled arc of 60 degrees. +local halfArc = math.rad(30) +local sampleArc = function(x) return 1 - math.cos(x * halfArc) end +local arcHeight = sampleArc(1) + +function M.update(dt, smoothedSpeed) + local speed = self:getCurrentSpeed() + speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high + totalMovement = totalMovement + speed * dt + if not M.enabled or camera.getMode() ~= camera.MODE.FirstPerson then + effectWeight = 0 + return + end + if self:isOnGround() then + effectWeight = math.min(1, effectWeight + dt * 5) + else + effectWeight = math.max(0, effectWeight - dt * 5) + end + + local doubleStepState = totalMovement / doubleStepLength + doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps + local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps + local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1 + + -- Smoothly reduce the effect to zero when the player stops + local coef = math.min(smoothedSpeed / 300, 1) * effectWeight + + local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2 + local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll + camera.setFirstPersonOffset(camera.getFirstPersonOffset() + util.vector3(0, 0, zOffset)) + camera.setRoll(camera.getRoll() + roll) +end + +return M + diff --git a/files/builtin_scripts/scripts/omw/third_person.lua b/files/builtin_scripts/scripts/omw/third_person.lua new file mode 100644 index 0000000000..ca00ef5ee9 --- /dev/null +++ b/files/builtin_scripts/scripts/omw/third_person.lua @@ -0,0 +1,139 @@ +local camera = require('openmw.camera') +local settings = require('openmw.settings') +local util = require('openmw.util') +local self = require('openmw.self') +local nearby = require('openmw.nearby') + +local MODE = camera.MODE +local STATE = { RightShoulder = 0, LeftShoulder = 1, Combat = 2, Swimming = 3 } + +local M = { + baseDistance = settings._getFloatFromSettingsCfg('Camera', 'third person camera distance'), + preferredDistance = 0, + standingPreview = false, + noOffsetControl = 0, +} + +local viewOverShoulder = settings._getBoolFromSettingsCfg('Camera', 'view over shoulder') +local autoSwitchShoulder = settings._getBoolFromSettingsCfg('Camera', 'auto switch shoulder') +local shoulderOffset = settings._getVector2FromSettingsCfg('Camera', 'view over shoulder offset') +local zoomOutWhenMoveCoef = settings._getFloatFromSettingsCfg('Camera', 'zoom out when move coef') + +local defaultShoulder = (shoulderOffset.x > 0 and STATE.RightShoulder) or STATE.LeftShoulder +local rightShoulderOffset = util.vector2(math.abs(shoulderOffset.x), shoulderOffset.y) +local leftShoulderOffset = util.vector2(-math.abs(shoulderOffset.x), shoulderOffset.y) +local combatOffset = util.vector2(0, 15) + +local state = defaultShoulder + +local rayOptions = {collisionType = nearby.COLLISION_TYPE.Default - nearby.COLLISION_TYPE.Actor} +local function ray(from, angle, limit) + local to = from + util.transform.rotateZ(angle) * util.vector3(0, limit, 0) + local res = nearby.castRay(from, to, rayOptions) + if res.hit then + return (res.hitPos - from):length() + else + return limit + end +end + +local function trySwitchShoulder() + local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit + local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance + + local pos = camera.getTrackedPosition() + local rayRight = ray(pos, camera.getYaw() + math.rad(90), limitToSwitchBack + 1) + local rayLeft = ray(pos, camera.getYaw() - math.rad(90), limitToSwitchBack + 1) + local rayRightForward = ray(pos, camera.getYaw() + math.rad(30), limitToSwitchBack + 1) + local rayLeftForward = ray(pos, camera.getYaw() - math.rad(30), limitToSwitchBack + 1) + + local distRight = math.min(rayRight, rayRightForward) + local distLeft = math.min(rayLeft, rayLeftForward) + + if distLeft < limitToSwitch and distRight > limitToSwitchBack then + state = STATE.RightShoulder + elseif distRight < limitToSwitch and distLeft > limitToSwitchBack then + state = STATE.LeftShoulder + elseif distRight > limitToSwitchBack and distLeft > limitToSwitchBack then + state = defaultShoulder + end +end + +local function calculateDistance(smoothedSpeed) + local smoothedSpeedSqr = smoothedSpeed * smoothedSpeed + return (M.baseDistance + math.max(camera.getPitch(), 0) * 50 + + smoothedSpeedSqr / (smoothedSpeedSqr + 300*300) * zoomOutWhenMoveCoef) +end + +local noThirdPersonLastFrame = true + +local function updateState() + local mode = camera.getMode() + local oldState = state + if (self:isInWeaponStance() or self:isInMagicStance()) and mode == MODE.ThirdPerson then + state = STATE.Combat + elseif self:isSwimming() then + state = STATE.Swimming + elseif oldState == STATE.Combat or oldState == STATE.Swimming then + state = defaultShoulder + end + if autoSwitchShoulder and (mode == MODE.ThirdPerson or state ~= oldState or noThirdPersonLastFrame) + and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then + trySwitchShoulder() + end + if oldState ~= state or noThirdPersonLastFrame then + -- State was changed, start focal point transition. + if mode == MODE.Vanity then + -- Player doesn't touch controls for a long time. Transition should be very slow. + camera.setFocalTransitionSpeed(0.2) + elseif (oldState == STATE.Combat or state == STATE.Combat) and + (mode ~= MODE.Preview or M.standingPreview) then + -- Transition to/from combat mode and we are not in preview mode. Should be fast. + camera.setFocalTransitionSpeed(5.0) + else + camera.setFocalTransitionSpeed(1.0) -- Default transition speed. + end + + if state == STATE.RightShoulder then + camera.setFocalPreferredOffset(rightShoulderOffset) + elseif state == STATE.LeftShoulder then + camera.setFocalPreferredOffset(leftShoulderOffset) + else + camera.setFocalPreferredOffset(combatOffset) + end + end +end + +function M.update(dt, smoothedSpeed) + local mode = camera.getMode() + if mode == MODE.FirstPerson or mode == MODE.Static then + noThirdPersonLastFrame = true + return + end + if not viewOverShoulder then + M.preferredDistance = M.baseDistance + camera.setPreferredThirdPersonDistance(M.baseDistance) + noThirdPersonLastFrame = false + return + end + + if M.noOffsetControl == 0 then + updateState() + else + state = nil + end + + M.preferredDistance = calculateDistance(smoothedSpeed) + if noThirdPersonLastFrame then -- just switched to third person view + camera.setPreferredThirdPersonDistance(M.preferredDistance) + camera.instantTransition() + noThirdPersonLastFrame = false + else + local maxIncrease = dt * (100 + M.baseDistance) + camera.setPreferredThirdPersonDistance(math.min( + M.preferredDistance, camera.getThirdPersonDistance() + maxIncrease)) + end +end + +return M + diff --git a/files/lua_api/openmw/camera.lua b/files/lua_api/openmw/camera.lua new file mode 100644 index 0000000000..0d72214414 --- /dev/null +++ b/files/lua_api/openmw/camera.lua @@ -0,0 +1,171 @@ +------------------------------------------------------------------------------- +-- `openmw.camera` controls camera. +-- Can be used only by player scripts. +-- @module camera +-- @usage local camera = require('openmw.camera') + + +------------------------------------------------------------------------------- +-- @type MODE Camera modes. +-- @field #number Static Camera doesn't track player; player inputs doesn't affect camera; use `setStaticPosition` to move the camera. +-- @field #number FirstPerson First person mode. +-- @field #number ThirdPerson Third person mode; player character turns to the view direction. +-- @field #number Vanity Similar to Preview; camera slowly moves around the player. +-- @field #number Preview Third person mode, but player character doesn't turn to the view direction. + +------------------------------------------------------------------------------- +-- Camera modes. +-- @field [parent=#camera] #MODE MODE + +------------------------------------------------------------------------------- +-- Return the current @{openmw.camera#MODE}. +-- @function [parent=#camera] getMode +-- @return #MODE + +------------------------------------------------------------------------------- +-- Return the mode the camera will switch to after the end of the current animation. Can be nil. +-- @function [parent=#camera] getQueuedMode +-- @return #MODE + +------------------------------------------------------------------------------- +-- Change @{openmw.camera#MODE}; if the second (optional, true by default) argument is set to false, the switching can be delayed (see `getQueuedMode`). +-- @function [parent=#camera] setMode +-- @param #MODE mode +-- @param #boolean force + +------------------------------------------------------------------------------- +-- If set to true then after switching from Preview to ThirdPerson the player character turns to the camera view direction. Otherwise the camera turns to the character view direction. +-- @function [parent=#camera] allowCharacterDeferredRotation +-- @param #boolean boolValue + +------------------------------------------------------------------------------- +-- Show/hide crosshair. +-- @function [parent=#camera] showCrosshair +-- @param #boolean boolValue + +------------------------------------------------------------------------------- +-- Current position of the tracked object (the characters head if there is no animation). +-- @function [parent=#camera] getTrackedPosition +-- @return openmw.util#Vector3 + +------------------------------------------------------------------------------- +-- Current position of the camera. +-- @function [parent=#camera] getPosition +-- @return openmw.util#Vector3 + +------------------------------------------------------------------------------- +-- Camera pitch angle (radians) without taking extraPitch into account. +-- Full pitch is `getPitch()+getExtraPitch()`. +-- @function [parent=#camera] getPitch +-- @return #number + +------------------------------------------------------------------------------- +-- Force the pitch angle to the given value (radians); player input on this axis is ignored in this frame. +-- @function [parent=#camera] setPitch +-- @param #number value + +------------------------------------------------------------------------------- +-- Camera yaw angle (radians) without taking extraYaw into account. +-- Full yaw is `getYaw()+getExtraYaw()`. +-- @function [parent=#camera] getYaw +-- @return #number + +------------------------------------------------------------------------------- +-- Force the yaw angle to the given value (radians); player input on this axis is ignored in this frame. +-- @function [parent=#camera] setYaw +-- @param #number value + +------------------------------------------------------------------------------- +-- Get camera roll angle (radians). +-- @function [parent=#camera] getRoll +-- @return #number + +------------------------------------------------------------------------------- +-- Set camera roll angle (radians). +-- @function [parent=#camera] setRoll +-- @param #number value + +------------------------------------------------------------------------------- +-- Additional summand for the pitch angle that is not affected by player input. +-- Full pitch is `getPitch()+getExtraPitch()`. +-- @function [parent=#camera] getExtraPitch +-- @return #number + +------------------------------------------------------------------------------- +-- Additional summand for the pitch angle; useful for camera shaking effects. +-- Setting extra pitch doesn't block player input. +-- Full pitch is `getPitch()+getExtraPitch()`. +-- @function [parent=#camera] setExtraPitch +-- @param #number value + +------------------------------------------------------------------------------- +-- Additional summand for the yaw angle that is not affected by player input. +-- Full yaw is `getYaw()+getExtraYaw()`. +-- @function [parent=#camera] getExtraYaw +-- @return #number + +------------------------------------------------------------------------------- +-- Additional summand for the yaw angle; useful for camera shaking effects. +-- Setting extra pitch doesn't block player input. +-- Full yaw is `getYaw()+getExtraYaw()`. +-- @function [parent=#camera] setExtraYaw +-- @param #number value + +------------------------------------------------------------------------------- +-- Set camera position; can be used only if camera is in Static mode. +-- @function [parent=#camera] setStaticPosition +-- @param openmw.util#Vector3 pos + +------------------------------------------------------------------------------- +-- The offset between the characters head and the camera in first person mode (3d vector). +-- @function [parent=#camera] getFirstPersonOffset +-- @return openmw.util#Vector3 + +------------------------------------------------------------------------------- +-- Set the offset between the characters head and the camera in first person mode (3d vector). +-- @function [parent=#camera] setFirstPersonOffset +-- @param openmw.util#Vector3 offset + +------------------------------------------------------------------------------- +-- Preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode. +-- See `setFocalPreferredOffset`. +-- @function [parent=#camera] getFocalPreferredOffset +-- @return openmw.util#Vector2 + +------------------------------------------------------------------------------- +-- Set preferred offset between tracked position (see `getTrackedPosition`) and the camera focal point (the center of the screen) in third person mode. +-- The offset is a 2d vector (X, Y) where X is horizontal (to the right from the character) and Y component is vertical (upward). +-- The real offset can differ from the preferred one during smooth transition of if blocked by an obstacle. +-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition. +-- @function [parent=#camera] setFocalPreferredOffset +-- @param openmw.util#Vector2 offset + +------------------------------------------------------------------------------- +-- The actual distance between the camera and the character in third person mode; can differ from the preferred one if there is an obstacle. +-- @function [parent=#camera] getThirdPersonDistance +-- @return #number + +------------------------------------------------------------------------------- +-- Set preferred distance between the camera and the character in third person mode. +-- @function [parent=#camera] setPreferredThirdPersonDistance +-- @param #number distance + +------------------------------------------------------------------------------- +-- The current speed coefficient of focal point (the center of the screen in third person mode) smooth transition. +-- @function [parent=#camera] getFocalTransitionSpeed +-- @return #number + +------------------------------------------------------------------------------- +-- Set the speed coefficient of focal point (the center of the screen in third person mode) smooth transition. +-- Smooth transition happens by default every time when the preferred offset was changed. Use `instantTransition()` to skip the current transition. +-- @function [parent=#camera] setFocalTransitionSpeed +-- Set the speed coefficient +-- @param #number speed + +------------------------------------------------------------------------------- +-- Make instant the current transition of camera focal point and the current deferred rotation (see `allowCharacterDeferredRotation`). +-- @function [parent=#camera] instantTransition + + +return nil + diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 50706b9770..35513bbb79 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -192,10 +192,26 @@ ------------------------------------------------------------------------------- -- Add new local script to the object. --- Can be called only from a global script. +-- Can be called only from a global script. Script should be specified in a content +-- file (omwgame/omwaddon/omwscripts) with a CUSTOM flag. -- @function [parent=#GameObject] addScript -- @param self --- @param #string scriptPath Path to the script in OpenMW virtual filesystem +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. + +------------------------------------------------------------------------------- +-- Whether a script with given path is attached to this object. +-- Can be called only from a global script. +-- @function [parent=#GameObject] hasScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. +-- @return #boolean + +------------------------------------------------------------------------------- +-- Removes script that was attached by `addScript` +-- Can be called only from a global script. +-- @function [parent=#GameObject] removeScript +-- @param self +-- @param #string scriptPath Path to the script in OpenMW virtual filesystem. ------------------------------------------------------------------------------- -- Moves object to given cell and position. diff --git a/files/lua_api/openmw/input.lua b/files/lua_api/openmw/input.lua index 0975ce6e6a..361073e79d 100644 --- a/files/lua_api/openmw/input.lua +++ b/files/lua_api/openmw/input.lua @@ -17,6 +17,38 @@ -- @param #number actionId One of @{openmw.input#ACTION} -- @return #boolean +------------------------------------------------------------------------------- +-- Is a keyboard button currently pressed. +-- @function [parent=#input] isKeyPressed +-- @param #number keyCode Key code (see @{openmw.input#KEY}) +-- @return #boolean + +------------------------------------------------------------------------------- +-- Is a controller button currently pressed. +-- @function [parent=#input] isControllerButtonPressed +-- @param #number buttonId Button index (see @{openmw.input#CONTROLLER_BUTTON}) +-- @return #boolean + +------------------------------------------------------------------------------- +-- Is `Shift` key pressed. +-- @function [parent=#input] isShiftPressed +-- @return #boolean + +------------------------------------------------------------------------------- +-- Is `Ctrl` key pressed. +-- @function [parent=#input] isCtrlPressed +-- @return #boolean + +------------------------------------------------------------------------------- +-- Is `Alt` key pressed. +-- @function [parent=#input] isAltPressed +-- @return #boolean + +------------------------------------------------------------------------------- +-- Is `Super`/`Win` key pressed. +-- @function [parent=#input] isSuperPressed +-- @return #boolean + ------------------------------------------------------------------------------- -- Is a mouse button currently pressed. -- @function [parent=#input] isMouseButtonPressed @@ -51,6 +83,12 @@ -- @param #string key Control type (see @{openmw.input#CONTROL_SWITCH}) -- @param #boolean value +------------------------------------------------------------------------------- +-- Returns a human readable name for the given key code +-- @function [parent=#input] getKeyName +-- @param #number code A key code (see @{openmw.input#KEY}) +-- @return #string + ------------------------------------------------------------------------------- -- @type CONTROL_SWITCH -- @field [parent=#CONTROL_SWITCH] #string Controls Ability to move @@ -156,10 +194,119 @@ -- Values that can be used with getAxisValue. -- @field [parent=#input] #CONTROLLER_AXIS CONTROLLER_AXIS +------------------------------------------------------------------------------- +-- @type KEY +-- @field #number _0 +-- @field #number _1 +-- @field #number _2 +-- @field #number _3 +-- @field #number _4 +-- @field #number _5 +-- @field #number _6 +-- @field #number _7 +-- @field #number _8 +-- @field #number _9 +-- @field #number NP_0 +-- @field #number NP_1 +-- @field #number NP_2 +-- @field #number NP_3 +-- @field #number NP_4 +-- @field #number NP_5 +-- @field #number NP_6 +-- @field #number NP_7 +-- @field #number NP_8 +-- @field #number NP_9 +-- @field #number NP_Divide +-- @field #number NP_Enter +-- @field #number NP_Minus +-- @field #number NP_Multiply +-- @field #number NP_Delete +-- @field #number NP_Plus +-- @field #number F1 +-- @field #number F2 +-- @field #number F3 +-- @field #number F4 +-- @field #number F5 +-- @field #number F6 +-- @field #number F7 +-- @field #number F8 +-- @field #number F9 +-- @field #number F10 +-- @field #number F11 +-- @field #number F12 +-- @field #number A +-- @field #number B +-- @field #number C +-- @field #number D +-- @field #number E +-- @field #number F +-- @field #number G +-- @field #number H +-- @field #number I +-- @field #number J +-- @field #number K +-- @field #number L +-- @field #number M +-- @field #number N +-- @field #number O +-- @field #number P +-- @field #number Q +-- @field #number R +-- @field #number S +-- @field #number T +-- @field #number U +-- @field #number V +-- @field #number W +-- @field #number X +-- @field #number Y +-- @field #number Z +-- @field #number LeftArrow +-- @field #number RightArrow +-- @field #number UpArrow +-- @field #number DownArrow +-- @field #number LeftAlt +-- @field #number LeftCtrl +-- @field #number LeftBracket +-- @field #number LeftSuper +-- @field #number LeftShift +-- @field #number RightAlt +-- @field #number RightCtrl +-- @field #number RightBracket +-- @field #number RightSuper +-- @field #number RightShift +-- @field #number Apostrophe +-- @field #number BackSlash +-- @field #number Backspace +-- @field #number CapsLock +-- @field #number Comma +-- @field #number Delete +-- @field #number End +-- @field #number Enter +-- @field #number Equals +-- @field #number Escape +-- @field #number Home +-- @field #number Insert +-- @field #number Minus +-- @field #number NumLock +-- @field #number PageDown +-- @field #number PageUp +-- @field #number Pause +-- @field #number Period +-- @field #number PrintScreen +-- @field #number ScrollLock +-- @field #number Semicolon +-- @field #number Slash +-- @field #number Space +-- @field #number Tab + +------------------------------------------------------------------------------- +-- Key codes. +-- @field [parent=#input] #KEY KEY + ------------------------------------------------------------------------------- -- The argument of `onKeyPress`/`onKeyRelease` engine handlers. -- @type KeyboardEvent --- @field [parent=#KeyboardEvent] #string symbol The pressed symbol (1-symbol string). +-- @field [parent=#KeyboardEvent] #string symbol The pressed symbol (1-symbol string if can be represented or an empty string otherwise). -- @field [parent=#KeyboardEvent] #string code Key code. -- @field [parent=#KeyboardEvent] #boolean withShift Is `Shift` key pressed. -- @field [parent=#KeyboardEvent] #boolean withCtrl Is `Control` key pressed. diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index bf6976c76b..593cbf8043 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -1,15 +1,115 @@ -------------------------------------------------------------------------------- +--- -- `openmw.ui` controls user interface. -- Can be used only by local scripts, that are attached to a player. -- @module ui --- @usage local ui = require('openmw.ui') +-- @usage +-- local ui = require('openmw.ui') +--- +-- @field [parent=#ui] #WIDGET_TYPE WIDGET_TYPE +--- +-- @type WIDGET_TYPE +-- @field [parent=#WIDGET_TYPE] Widget Base widget type +-- @field [parent=#WIDGET_TYPE] Text Display text +-- @field [parent=#WIDGET_TYPE] TextEdit Accepts user text input +-- @field [parent=#WIDGET_TYPE] Window Can be moved and resized by the user -------------------------------------------------------------------------------- +--- -- Shows given message at the bottom of the screen. -- @function [parent=#ui] showMessage -- @param #string msg -return nil +--- +-- Converts a given table of tables into an @{openmw.ui#Content} +-- @function [parent=#ui] content +-- @param #table table +-- @return #Content + +--- +-- Creates a UI element from the given layout table +-- @function [parent=#ui] create +-- @param #Layout layout +-- @return #Element + +--- +-- Layout +-- @type Layout +-- @field #string name Optional name of the layout. Allows access by name from Content +-- @field #table props Optional table of widget properties +-- @field #table events Optional table of event callbacks +-- @field #Content content Optional @{openmw.ui#Content} of children layouts + +--- +-- Content. An array-like container, which allows to reference elements by their name +-- @type Content +-- @list <#Layout> +-- @usage +-- local content = ui.content { +-- { name = 'input' }, +-- } +-- -- bad idea! +-- -- content[1].name = 'otherInput' +-- -- do this instead: +-- content.input = { name = 'otherInput' } +-- @usage +-- local content = ui.content { +-- { name = 'display' }, +-- { name = 'submit' }, +-- } +-- -- allowed, but shifts all the items after it "up" the array +-- content.display = nil +-- -- still no holes after this! +-- @usage +-- -- iterate over a Content +-- for i = 1, #content do +-- print('widget',content[i].name,'at',i) +-- end + +--- +-- Puts the layout at given index by shifting all the elements after it +-- @function [parent=#Content] insert +-- @param self +-- @param #number index +-- @param #Layout layout + +--- +-- Adds the layout at the end of the Content +-- (same as calling insert with `last index + 1`) +-- @function [parent=#Content] add +-- @param self +-- @param #Layout layout + +--- +-- Finds the index of the given layout. If it is not in the container, returns nil +-- @function [parent=#Content] indexOf +-- @param self +-- @param #Layout layout +-- @return #number, #nil index +--- +-- Element. An element of the user interface +-- @type Element + +--- +-- Refreshes the rendered element to match the current layout state +-- @function [parent=#Element] update +-- @param self + +--- +-- Destroys the element +-- @function [parent=#Element] destroy +-- @param self + +--- +-- Access or replace the element's layout +-- @field [parent=#Element] #Layout layout + +--- +-- Mouse event, passed as an argument to relevant UI events +-- @type MouseEvent +-- @field openmw.util#Vector2 position Absolute position of the mouse cursor +-- @field openmw.util#Vector2 offset Position of the mouse cursor relative to the widget +-- @field #number button Mouse button which triggered the event (could be nil) + +return nil diff --git a/files/lua_api/openmw/util.lua b/files/lua_api/openmw/util.lua index 84e640ff40..fdb140ad53 100644 --- a/files/lua_api/openmw/util.lua +++ b/files/lua_api/openmw/util.lua @@ -32,7 +32,7 @@ -- v:length() -- 5.0 length -- v:length2() -- 25.0 square of the length -- v:normalize() -- vector2(3/5, 4/5) --- v:rotate(radians) -- rotate clockwise (returns rotated vector) +-- v:rotate(radians) -- rotate counterclockwise (returns rotated vector) -- v1:dot(v2) -- dot product (returns a number) -- v1 * v2 -- dot product -- v1 + v2 -- vector addition @@ -183,26 +183,26 @@ ------------------------------------------------------------------------------- --- Rotation (any axis). +-- Rotation around a vector (counterclockwise if the vector points to us). -- @function [parent=#TRANSFORM] rotate -- @param #number angle -- @param #Vector3 axis. -- @return #Transform. ------------------------------------------------------------------------------- --- X-axis rotation. +-- X-axis rotation (equivalent to `rotate(angle, vector3(-1, 0, 0))`). -- @function [parent=#TRANSFORM] rotateX -- @param #number angle -- @return #Transform. ------------------------------------------------------------------------------- --- Y-axis rotation. +-- Y-axis rotation (equivalent to `rotate(angle, vector3(0, -1, 0))`). -- @function [parent=#TRANSFORM] rotateY -- @param #number angle -- @return #Transform. ------------------------------------------------------------------------------- --- Z-axis rotation. +-- Z-axis rotation (equivalent to `rotate(angle, vector3(0, 0, -1))`). -- @function [parent=#TRANSFORM] rotateZ -- @param #number angle -- @return #Transform. diff --git a/files/mygui/CMakeLists.txt b/files/mygui/CMakeLists.txt index 49b833e382..bdf7558cf7 100644 --- a/files/mygui/CMakeLists.txt +++ b/files/mygui/CMakeLists.txt @@ -1,4 +1,4 @@ -if (NOT DEFINED OPENMW_MYGUI_FILES_ROOT) +if (NOT DEFINED OPENMW_RESOURCES_ROOT) return() endif() @@ -97,4 +97,4 @@ set(MYGUI_FILES ) -copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_MYGUI_FILES_ROOT} ${DDIRRELATIVE} "${MYGUI_FILES}") +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${MYGUI_FILES}") diff --git a/files/mygui/openmw_hud.layout b/files/mygui/openmw_hud.layout index b3e3cfb9df..a189075f44 100644 --- a/files/mygui/openmw_hud.layout +++ b/files/mygui/openmw_hud.layout @@ -31,22 +31,6 @@ - - - - - - - - - - - - - - - - @@ -128,4 +112,23 @@ - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/files/mygui/openmw_layers.xml b/files/mygui/openmw_layers.xml index c288655027..4afa67f314 100644 --- a/files/mygui/openmw_layers.xml +++ b/files/mygui/openmw_layers.xml @@ -15,6 +15,7 @@ + diff --git a/files/mygui/openmw_resources.xml b/files/mygui/openmw_resources.xml index 811fa4f7fa..ff4893a9b2 100644 --- a/files/mygui/openmw_resources.xml +++ b/files/mygui/openmw_resources.xml @@ -47,7 +47,13 @@ - + + + + + + + diff --git a/files/mygui/openmw_settings_window.layout b/files/mygui/openmw_settings_window.layout index a07d8125d5..8e81764f60 100644 --- a/files/mygui/openmw_settings_window.layout +++ b/files/mygui/openmw_settings_window.layout @@ -454,6 +454,16 @@ + + + + + + + + + + diff --git a/files/openmw.cfg b/files/openmw.cfg index afbf0810cc..d98097c3eb 100644 --- a/files/openmw.cfg +++ b/files/openmw.cfg @@ -2,6 +2,7 @@ # Modifications should be done on the user openmw.cfg file instead # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) +content=builtin.omwscripts data=${MORROWIND_DATA_FILES} data-local="?userdata?data" resources=${OPENMW_RESOURCE_FILES} diff --git a/files/openmw.cfg.local b/files/openmw.cfg.local index 76f829379b..704ac68068 100644 --- a/files/openmw.cfg.local +++ b/files/openmw.cfg.local @@ -2,6 +2,7 @@ # Modifications should be done on the user openmw.cfg file instead # (see: https://openmw.readthedocs.io/en/master/reference/modding/paths.html) +content=builtin.omwscripts data="?global?data" data=./data data-local="?userdata?data" diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 5ed8109465..9715e3793e 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -373,6 +373,9 @@ graphic herbalism = true # (true, false) allow actors to follow over water surface = true +# Default size of actor for navmesh generation +default actor pathfind half extents = 29.27999496459961 28.479997634887695 66.5 + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). @@ -491,6 +494,9 @@ minimum interior brightness = 0.08 # When MSAA is off, this setting will have no visible effect, but might have a performance cost. antialias alpha test = false +# Soften intersection of blended particle systems with opaque geometry +soft particles = false + [Input] # Capture control of the cursor prevent movement outside the window. @@ -646,6 +652,10 @@ refraction = false # Draw objects on water reflections. reflection detail = 2 +# Whether to use fully detailed raindrop ripples. (0, 1, 2). +# 0 = rings only; 1 = sparse, high detail; 2 = dense, high detail +rain ripple detail = 2 + # Overrides the value in '[Camera] small feature culling pixel size' specifically for water reflection/refraction textures. small feature culling pixel size = 20.0 @@ -1096,6 +1106,9 @@ stomp intensity = 1 [Lua] +# Enable performance-heavy debug features +lua debug = false + # Set the maximum number of threads used for Lua scripts. # If zero, Lua scripts are processed in the main thread. lua num threads = 1 diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 04446d2982..73929486cd 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -1,4 +1,4 @@ -if (NOT DEFINED OPENMW_SHADERS_ROOT) +if (NOT DEFINED OPENMW_RESOURCES_ROOT) return() endif() @@ -36,6 +36,10 @@ set(SHADER_FILES gui_fragment.glsl debug_vertex.glsl debug_fragment.glsl + sky_vertex.glsl + sky_fragment.glsl + skypasses.glsl + softparticles.glsl ) -copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/nv_default_fragment.glsl b/files/shaders/nv_default_fragment.glsl index 245f83b620..ff81a2b94d 100644 --- a/files/shaders/nv_default_fragment.glsl +++ b/files/shaders/nv_default_fragment.glsl @@ -39,12 +39,13 @@ varying vec3 passNormal; #include "alpha.glsl" uniform float emissiveMult; +uniform float specStrength; void main() { #if @diffuseMap gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV); - gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, adjustedDiffuseUV); + gl_FragData[0].a *= coveragePreservingAlphaScale(diffuseMap, diffuseMapUV); #else gl_FragData[0] = vec4(1.0); #endif @@ -80,7 +81,7 @@ void main() gl_FragData[0].xyz *= lighting; float shininess = gl_FrontMaterial.shininess; - vec3 matSpec = getSpecularColor().xyz; + vec3 matSpec = getSpecularColor().xyz * specStrength; #if @normalMap matSpec *= normalTex.a; #endif diff --git a/files/shaders/objects_fragment.glsl b/files/shaders/objects_fragment.glsl index 6f6cede4e4..99ed44919b 100644 --- a/files/shaders/objects_fragment.glsl +++ b/files/shaders/objects_fragment.glsl @@ -71,6 +71,7 @@ centroid varying vec3 shadowDiffuseLighting; #else uniform float emissiveMult; #endif +uniform float specStrength; varying vec3 passViewPos; varying vec3 passNormal; @@ -80,6 +81,10 @@ varying vec3 passNormal; #include "parallax.glsl" #include "alpha.glsl" +#if @softParticles +#include "softparticles.glsl" +#endif + void main() { #if @diffuseMap @@ -200,6 +205,7 @@ void main() vec3 matSpec = getSpecularColor().xyz; #endif + matSpec *= specStrength; if (matSpec != vec3(0.0)) { #if (!@normalMap && !@parallax && !@forcePPL) @@ -220,6 +226,10 @@ void main() #endif gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); +#if @softParticles + gl_FragData[0].a *= calcSoftParticleFade(); +#endif + #if defined(FORCE_OPAQUE) && FORCE_OPAQUE // having testing & blending isn't enough - we need to write an opaque pixel to be opaque gl_FragData[0].a = 1.0; diff --git a/files/shaders/sky_fragment.glsl b/files/shaders/sky_fragment.glsl new file mode 100644 index 0000000000..cfa3650c02 --- /dev/null +++ b/files/shaders/sky_fragment.glsl @@ -0,0 +1,87 @@ +#version 120 + +#include "skypasses.glsl" + +uniform int pass; +uniform sampler2D diffuseMap; +uniform sampler2D maskMap; // PASS_MOON +uniform float opacity; // PASS_CLOUDS, PASS_ATMOSPHERE_NIGHT +uniform vec4 moonBlend; // PASS_MOON +uniform vec4 atmosphereFade; // PASS_MOON + +varying vec2 diffuseMapUV; +varying vec4 passColor; + +void paintAtmosphere(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a *= passColor.a; +} + +void paintAtmosphereNight(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; +} + +void paintClouds(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= passColor.a * opacity; + color.xyz = clamp(color.xyz * gl_FrontMaterial.emission.xyz, 0.0, 1.0); + + // ease transition between clear color and atmosphere/clouds + color = mix(vec4(gl_Fog.color.xyz, color.a), color, passColor.a); +} + +void paintMoon(inout vec4 color) +{ + vec4 phase = texture2D(diffuseMap, diffuseMapUV); + vec4 mask = texture2D(maskMap, diffuseMapUV); + + vec4 blendedLayer = phase * moonBlend; + color = vec4(blendedLayer.xyz + atmosphereFade.xyz, atmosphereFade.a * mask.a); +} + +void paintSun(inout vec4 color) +{ + color = texture2D(diffuseMap, diffuseMapUV); + color.a *= gl_FrontMaterial.diffuse.a; +} + +void paintSunflashQuery(inout vec4 color) +{ + const float threshold = 0.8; + + color = texture2D(diffuseMap, diffuseMapUV); + if (color.a <= threshold) + discard; +} + +void paintSunglare(inout vec4 color) +{ + color = gl_FrontMaterial.emission; + color.a = gl_FrontMaterial.diffuse.a; +} + +void main() +{ + vec4 color = vec4(0.0); + + if (pass == PASS_ATMOSPHERE) + paintAtmosphere(color); + else if (pass == PASS_ATMOSPHERE_NIGHT) + paintAtmosphereNight(color); + else if (pass == PASS_CLOUDS) + paintClouds(color); + else if (pass == PASS_MOON) + paintMoon(color); + else if (pass == PASS_SUN) + paintSun(color); + else if (pass == PASS_SUNFLASH_QUERY) + paintSunflashQuery(color); + else if (pass == PASS_SUNGLARE) + paintSunglare(color); + + gl_FragData[0] = color; +} diff --git a/files/shaders/sky_vertex.glsl b/files/shaders/sky_vertex.glsl new file mode 100644 index 0000000000..9c676140ac --- /dev/null +++ b/files/shaders/sky_vertex.glsl @@ -0,0 +1,20 @@ +#version 120 + +#include "skypasses.glsl" + +uniform mat4 projectionMatrix; +uniform int pass; + +varying vec4 passColor; +varying vec2 diffuseMapUV; + +void main() +{ + gl_Position = projectionMatrix * (gl_ModelViewMatrix * gl_Vertex); + passColor = gl_Color; + + if (pass == PASS_CLOUDS) + diffuseMapUV = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy; + else + diffuseMapUV = gl_MultiTexCoord0.xy; +} diff --git a/files/shaders/skypasses.glsl b/files/shaders/skypasses.glsl new file mode 100644 index 0000000000..e80d4eb259 --- /dev/null +++ b/files/shaders/skypasses.glsl @@ -0,0 +1,7 @@ +#define PASS_ATMOSPHERE 0 +#define PASS_ATMOSPHERE_NIGHT 1 +#define PASS_CLOUDS 2 +#define PASS_MOON 3 +#define PASS_SUN 4 +#define PASS_SUNFLASH_QUERY 5 +#define PASS_SUNGLARE 6 diff --git a/files/shaders/softparticles.glsl b/files/shaders/softparticles.glsl new file mode 100644 index 0000000000..fa8b4de4c1 --- /dev/null +++ b/files/shaders/softparticles.glsl @@ -0,0 +1,32 @@ +uniform float near; +uniform float far; +uniform sampler2D opaqueDepthTex; +uniform vec2 screenRes; +uniform float particleSize; + +float viewDepth(float depth) +{ +#if @reverseZ + depth = 1.0 - depth; +#endif + return (near * far) / ((far - near) * depth - far); +} + +float calcSoftParticleFade() +{ + const float falloffMultiplier = 0.33; + const float contrast = 1.30; + + vec2 screenCoords = gl_FragCoord.xy / screenRes; + float sceneDepth = viewDepth(texture2D(opaqueDepthTex, screenCoords).x); + float particleDepth = viewDepth(gl_FragCoord.z); + float falloff = particleSize * falloffMultiplier; + float delta = particleDepth - sceneDepth; + + if (delta < 0.0) + discard; + + const float shift = 0.845; + + return shift * pow(clamp(delta/falloff, 0.0, 1.0), contrast); +} diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl index 26f83e052c..eaf52c1189 100644 --- a/files/shaders/water_fragment.glsl +++ b/files/shaders/water_fragment.glsl @@ -9,6 +9,7 @@ #endif #define REFRACTION @refraction_enabled +#define RAIN_RIPPLE_DETAIL @rain_ripple_detail // Inspired by Blender GLSL Water by martinsh ( https://devlog-martinsh.blogspot.de/2012/07/waterundewater-shader-wip.html ) @@ -55,14 +56,22 @@ const float WOBBLY_SHORE_FADE_DISTANCE = 6200.0; // fade out wobbly shores to // ---------------- rain ripples related stuff --------------------- -const float RAIN_RIPPLE_GAPS = 5.0; -const float RAIN_RIPPLE_RADIUS = 0.1; +const float RAIN_RIPPLE_GAPS = 10.0; +const float RAIN_RIPPLE_RADIUS = 0.2; -vec2 randOffset(vec2 c) +float scramble(float x, float z) { - return fract(vec2( - c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2, - c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7)); + return fract(pow(fract(x)*3.0+1.0, z)); +} + +vec2 randOffset(vec2 c, float time) +{ + time = fract(time/1000.0); + c = vec2(c.x * c.y / 8.0 + c.y * 0.3 + c.x * 0.2, + c.x * c.y / 14.0 + c.y * 0.5 + c.x * 0.7); + c.x *= scramble(scramble(time + c.x/1000.0, 4.0), 3.0) + 1.0; + c.y *= scramble(scramble(time + c.y/1000.0, 3.5), 3.0) + 1.0; + return fract(c); } float randPhase(vec2 c) @@ -70,43 +79,104 @@ float randPhase(vec2 c) return fract((c.x * c.y) / (c.x + c.y + 0.1)); } -vec4 circle(vec2 coords, vec2 i_part, float phase) +float blip(float x) { - vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(i_part) - 1.0); - vec2 toCenter = coords - center; - float d = length(toCenter); + x = max(0.0, 1.0-x*x); + return x*x*x; +} - float r = RAIN_RIPPLE_RADIUS * phase; +float blipDerivative(float x) +{ + x = clamp(x, -1.0, 1.0); + float n = x*x-1.0; + return -6.0*x*n*n; +} - if (d > r) - return vec4(0.0,0.0,1.0,0.0); +const float RAIN_RING_TIME_OFFSET = 1.0/6.0; - float sinValue = (sin(d / r * 1.2) + 0.7) / 2.0; +vec4 circle(vec2 coords, vec2 corner, float adjusted_time) +{ + vec2 center = vec2(0.5,0.5) + (0.5 - RAIN_RIPPLE_RADIUS) * (2.0 * randOffset(corner, floor(adjusted_time)) - 1.0); + float phase = fract(adjusted_time); + vec2 toCenter = coords - center; - float height = (1.0 - abs(phase)) * pow(sinValue,3.0); + float r = RAIN_RIPPLE_RADIUS; + float d = length(toCenter); + float ringfollower = (phase-d/r)/RAIN_RING_TIME_OFFSET-1.0; // -1.0 ~ +1.0 cover the breadth of the ripple's ring + +#if RAIN_RIPPLE_DETAIL > 0 +// normal mapped ripples + if(ringfollower < -1.0 || ringfollower > 1.0) + return vec4(0.0); + + if(d > 1.0) // normalize center direction vector, but not for near-center ripples + toCenter /= d; + + float height = blip(ringfollower*2.0+0.5); // brighten up outer edge of ring; for fake specularity + float range_limit = blip(min(0.0, ringfollower)); + float energy = 1.0-phase; + + vec2 normal2d = -toCenter*blipDerivative(ringfollower)*5.0; + vec3 normal = vec3(normal2d, 0.5); + vec4 ret = vec4(normal, height); + ret.xyw *= energy*energy; + // do energy adjustment here rather than later, so that we can use the w component for fake specularity + ret.xyz = normalize(ret.xyz) * energy*range_limit; + ret.z *= range_limit; + return ret; +#else +// ring-only ripples + if(ringfollower < -1.0 || ringfollower > 0.5) + return vec4(0.0); - vec3 normal = normalize(mix(vec3(0.0,0.0,1.0),vec3(normalize(toCenter),0.0),height)); + float energy = 1.0-phase; + float height = blip(ringfollower*2.0+0.5)*energy*energy; // fake specularity - return vec4(normal,height); + return vec4(0.0, 0.0, 0.0, height); +#endif } - vec4 rain(vec2 uv, float time) { - vec2 i_part = floor(uv * RAIN_RIPPLE_GAPS); - vec2 f_part = fract(uv * RAIN_RIPPLE_GAPS); - return circle(f_part,i_part,fract(time * 1.2 + randPhase(i_part))); + uv *= RAIN_RIPPLE_GAPS; + vec2 f_part = fract(uv); + vec2 i_part = floor(uv); + float adjusted_time = time * 1.2 + randPhase(i_part); +#if RAIN_RIPPLE_DETAIL > 0 + vec4 a = circle(f_part, i_part, adjusted_time); + vec4 b = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET); + vec4 c = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*2.0); + vec4 d = circle(f_part, i_part, adjusted_time - RAIN_RING_TIME_OFFSET*3.0); + vec4 ret; + ret.xy = a.xy - b.xy/2.0 + c.xy/4.0 - d.xy/8.0; + // z should always point up + ret.z = a.z + b.z /2.0 + c.z /4.0 + d.z /8.0; + //ret.xyz *= 1.5; + // fake specularity looks weird if we use every single ring, also if the inner rings are too bright + ret.w = (a.w + c.w /8.0)*1.5; + return ret; +#else + return circle(f_part, i_part, adjusted_time) * 1.5; +#endif } -vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and ripple height in w +vec2 complex_mult(vec2 a, vec2 b) +{ + return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x); +} +vec4 rainCombined(vec2 uv, float time) // returns ripple normal in xyz and fake specularity in w { return - rain(uv,time) + - rain(uv + vec2(10.5,5.7),time) + - rain(uv * 0.75 + vec2(3.7,18.9),time) + - rain(uv * 0.9 + vec2(5.7,30.1),time) + - rain(uv * 0.8 + vec2(1.2,3.0),time); + rain(uv, time) + + rain(complex_mult(uv, vec2(0.4, 0.7)) + vec2(1.2, 3.0),time) + #if RAIN_RIPPLE_DETAIL == 2 + + rain(uv * 0.75 + vec2( 3.7,18.9),time) + + rain(uv * 0.9 + vec2( 5.7,30.1),time) + + rain(uv * 1.0 + vec2(10.5 ,5.7),time) + #endif + ; } + // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - float fresnel_dielectric(vec3 Incoming, vec3 Normal, float eta) @@ -132,7 +202,6 @@ vec2 normalCoords(vec2 uv, float scale, float speed, float time, float timer1, f return uv * (WAVE_SCALE * scale) + WIND_DIR * time * (WIND_SPEED * speed) -(previousNormal.xy/previousNormal.zz) * WAVE_CHOPPYNESS + vec2(time * timer1,time * timer2); } -varying vec3 screenCoordsPassthrough; varying vec4 position; varying float linearDepth; @@ -152,6 +221,8 @@ uniform vec3 nodePosition; uniform float rainIntensity; +uniform vec2 screenRes; + #define PER_PIXEL_LIGHTING 0 #include "shadows_fragment.glsl" @@ -178,8 +249,7 @@ void main(void) float shadow = unshadowedLightRatio(linearDepth); - vec2 screenCoords = screenCoordsPassthrough.xy / screenCoordsPassthrough.z; - screenCoords.y = (1.0-screenCoords.y); + vec2 screenCoords = gl_FragCoord.xy / screenRes; #define waterTimer osg_SimulationTime @@ -193,11 +263,11 @@ void main(void) vec4 rainRipple; if (rainIntensity > 0.01) - rainRipple = rainCombined(position.xy / 1000.0,waterTimer) * clamp(rainIntensity,0.0,1.0); + rainRipple = rainCombined(position.xy/1000.0, waterTimer) * clamp(rainIntensity, 0.0, 1.0); else rainRipple = vec4(0.0); - vec3 rippleAdd = rainRipple.xyz * rainRipple.w * 10.0; + vec3 rippleAdd = rainRipple.xyz * 10.0; vec2 bigWaves = vec2(BIG_WAVES_X,BIG_WAVES_Y); vec2 midWaves = mix(vec2(MID_WAVES_X,MID_WAVES_Y),vec2(MID_WAVES_RAIN_X,MID_WAVES_RAIN_Y),rainIntensity); @@ -245,7 +315,14 @@ void main(void) vec4 sunSpec = lcalcSpecular(0); + // artificial specularity to make rain ripples more noticeable + vec3 skyColorEstimate = vec3(max(0.0, mix(-0.3, 1.0, sunFade))); + vec3 rainSpecular = abs(rainRipple.w)*mix(skyColorEstimate, vec3(1.0), 0.05)*0.5; + #if REFRACTION + // no alpha here, so make sure raindrop ripple specularity gets properly subdued + rainSpecular *= clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); + // refraction vec3 refraction = texture2D(refractionMap, screenCoords - screenCoordsOffset).rgb; vec3 rawRefraction = refraction; @@ -265,7 +342,7 @@ 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 * sunSpec.xyz + vec3(rainRipple.w) * 0.2; + gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * sunSpec.xyz + rainSpecular; gl_FragData[0].w = 1.0; // wobbly water: hard-fade into refraction texture at extremely low depth, with a wobble based on normal mapping @@ -278,7 +355,7 @@ void main(void) shoreOffset = clamp(mix(shoreOffset, 1.0, clamp(linearDepth / WOBBLY_SHORE_FADE_DISTANCE, 0.0, 1.0)), 0.0, 1.0); gl_FragData[0].xyz = mix(rawRefraction, gl_FragData[0].xyz, shoreOffset); #else - gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + vec3(rainRipple.w) * 0.7; + gl_FragData[0].xyz = mix(reflection, waterColor, (1.0-fresnel)*0.5) + specular * sunSpec.xyz + rainSpecular; gl_FragData[0].w = clamp(fresnel*6.0 + specular * sunSpec.w, 0.0, 1.0); //clamp(fresnel*2.0 + specular * gl_LightSource[0].specular.w, 0.0, 1.0); #endif @@ -288,7 +365,7 @@ void main(void) #else float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); #endif - gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); + gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); applyShadowDebugOverlay(); } diff --git a/files/shaders/water_vertex.glsl b/files/shaders/water_vertex.glsl index 8e506a57f8..3a6c352ac8 100644 --- a/files/shaders/water_vertex.glsl +++ b/files/shaders/water_vertex.glsl @@ -2,7 +2,6 @@ uniform mat4 projectionMatrix; -varying vec3 screenCoordsPassthrough; varying vec4 position; varying float linearDepth; @@ -13,14 +12,6 @@ void main(void) { gl_Position = projectionMatrix * (gl_ModelViewMatrix * gl_Vertex); - mat4 scalemat = mat4(0.5, 0.0, 0.0, 0.0, - 0.0, -0.5, 0.0, 0.0, - 0.0, 0.0, 0.5, 0.0, - 0.5, 0.5, 0.5, 1.0); - - vec4 texcoordProj = ((scalemat) * ( gl_Position)); - screenCoordsPassthrough = texcoordProj.xyw; - position = gl_Vertex; vec4 viewPos = gl_ModelViewMatrix * gl_Vertex; diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 7964399319..3a2e7c49fc 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -14,7 +14,7 @@ - 0 + 1 @@ -281,8 +281,8 @@ 0 0 - 645 - 413 + 665 + 525 @@ -444,6 +444,26 @@ + + + + <html><head/><body><p>Enables soft particles for particle effects. This technique softens the intersection between individual particles and other opaque geometry by blending between them.</p></body></html> + + + Soft Particles + + + + + + + <html><head/><body><p>Allows MSAA to work with alpha-tested meshes, producing better-looking edges without pixelation. Can negatively impact performance.</p></body></html> + + + Use anti-alias alpha testing + + + diff --git a/files/vfs/CMakeLists.txt b/files/vfs/CMakeLists.txt index a97210d1df..15dcb80ec1 100644 --- a/files/vfs/CMakeLists.txt +++ b/files/vfs/CMakeLists.txt @@ -1,4 +1,4 @@ -if (NOT DEFINED OPENMW_MYGUI_FILES_ROOT) +if (NOT DEFINED OPENMW_RESOURCES_ROOT) return() endif() @@ -15,4 +15,4 @@ set(TEXTURE_FILES textures/omw_menu_scroll_center_v.dds ) -copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_MYGUI_FILES_ROOT} ${DDIRRELATIVE} "${TEXTURE_FILES}") +copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${TEXTURE_FILES}") diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index e53e54b785..0000000000 --- a/readthedocs.yml +++ /dev/null @@ -1,2 +0,0 @@ -# Don't build any extra formats -formats: [] \ No newline at end of file diff --git a/scripts/find_missing_merge_requests.py b/scripts/find_missing_merge_requests.py index 09d3e9a581..be54750098 100755 --- a/scripts/find_missing_merge_requests.py +++ b/scripts/find_missing_merge_requests.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import click +import discord_webhook import multiprocessing +import os import pathlib import requests import urllib.parse @@ -24,18 +26,23 @@ import urllib.parse help='End before given /merge_requests page.') @click.option('--per_page', type=int, default=100, help='Number of merge requests per page.') -def main(token_path, project_id, host, workers, target_branch, begin_page, end_page, per_page): - token = read_token(token_path) +@click.option('--ignored_mrs_path', type=str, + help='Path to a list of ignored MRs.') +def main(token_path, project_id, host, workers, target_branch, begin_page, end_page, per_page, ignored_mrs_path): + headers = make_headers(token_path) base_url = f'https://{host}/api/v4/projects/{project_id}/' + discord_webhook_url = os.getenv('DISCORD_WEBHOOK_URL') + ignored_mrs = frozenset(read_ignored_mrs(ignored_mrs_path)) checked = 0 filtered = 0 missing = 0 + missing_mrs = list() for page in range(begin_page, end_page): - merge_requests = requests.get( + merge_requests = parse_gitlab_response(requests.get( url=urllib.parse.urljoin(base_url, 'merge_requests'), - headers={'PRIVATE-TOKEN': token}, + headers=headers, params=dict(state='merged', per_page=per_page, page=page), - ).json() + )) if not merge_requests: break checked += len(merge_requests) @@ -44,24 +51,63 @@ def main(token_path, project_id, host, workers, target_branch, begin_page, end_p continue filtered += len(merge_requests) with multiprocessing.Pool(workers) as pool: - missing_merge_requests = pool.map(FilterMissingMergeRequest(token, base_url), merge_requests) + missing_merge_requests = pool.map(FilterMissingMergeRequest(headers, base_url), merge_requests) for mr in missing_merge_requests: - if mr is not None: - missing += 1 - print(f"MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']}," + if mr is None: + continue + if mr['reference'] in ignored_mrs or mr['reference'].strip('!') in ignored_mrs: + print(f"Ignored MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']}," f" previously was merged as {mr['merge_commit_sha']}") + continue + missing += 1 + missing_mrs.append(mr) + print(f"MR {mr['reference']} ({mr['id']}) is missing from branch {mr['target_branch']}," + f" previously was merged as {mr['merge_commit_sha']}") print(f'Checked {checked} MRs ({filtered} with {target_branch} target branch), {missing} are missing') + if discord_webhook_url is not None and missing_mrs: + project_web_url = parse_gitlab_response(requests.get(url=base_url, headers=headers))['web_url'] + '/' + discord_message = format_discord_message(missing=missing, filtered=filtered, target_branch=target_branch, + project_web_url=project_web_url, missing_mrs=missing_mrs) + print('Sending Discord notification...') + print(discord_message) + discord_webhook.DiscordWebhook(url=discord_webhook_url, content=discord_message, rate_limit_retry=True).execute() + if missing_mrs: + exit(-1) + + +def format_discord_message(missing, filtered, target_branch, project_web_url, missing_mrs): + target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}')) + return ( + f'Found {missing} missing MRs out of {filtered} from {target_branch} target branch:\n' + + '\n'.join(format_missing_mr_message(v, project_web_url) for v in missing_mrs) + ) + + +def format_missing_mr_message(mr, project_web_url): + web_url = mr.get('web_url') + reference = mr['reference'] + target_branch = mr['target_branch'] + commit = mr['merge_commit_sha'] + if web_url is not None: + reference = format_link(reference, web_url) + target_branch = format_link(target_branch, urllib.parse.urljoin(project_web_url, f'-/tree/{target_branch}')) + commit = format_link(commit, urllib.parse.urljoin(project_web_url, f'-/commit/{commit}')) + return f"MR {reference} is missing from branch {target_branch}, previously was merged as {commit}" + + +def format_link(name, url): + return f'[{name}]({url})' class FilterMissingMergeRequest: - def __init__(self, token, base_url): - self.token = token + def __init__(self, headers, base_url): + self.headers = headers self.base_url = base_url def __call__(self, merge_request): commit_refs = requests.get( url=urllib.parse.urljoin(self.base_url, f"repository/commits/{merge_request['merge_commit_sha']}/refs"), - headers={'PRIVATE-TOKEN': self.token}, + headers=self.headers, ).json() if 'message' in commit_refs and commit_refs['message'] == '404 Commit Not Found': return merge_request @@ -73,10 +119,39 @@ def present_in_branch(commit_refs, branch): return bool(next((v for v in commit_refs if v['type'] == 'branch' and v['name'] == branch), None)) +def make_headers(token_path): + job_token = os.environ.get('CI_JOB_TOKEN') + if job_token is not None: + print('Using auth token from CI_JOB_TOKEN env') + return {'JOB_TOKEN': job_token} + if not os.path.exists(token_path): + print(f'Ignore absent token path: {token_path}') + return dict() + print(f'Using auth token from: {token_path}') + return {'PRIVATE-TOKEN': read_token(token_path)} + + +def read_ignored_mrs(path): + if path is None: + return + with open(path) as stream: + for line in stream: + yield line.strip() + + def read_token(path): with open(path) as stream: return stream.readline().strip() +def parse_gitlab_response(response): + response = response.json() + if isinstance(response, dict): + message = response.get('message') + if message is not None: + raise RuntimeError(f'Gitlab request has failed: {message}') + return response + + if __name__ == '__main__': main() diff --git a/scripts/osg_stats.py b/scripts/osg_stats.py index 037aafea00..275e70dcff 100755 --- a/scripts/osg_stats.py +++ b/scripts/osg_stats.py @@ -12,11 +12,14 @@ import numpy import statistics import sys import termtables +import re @click.command() @click.option('--print_keys', is_flag=True, help='Print a list of all present keys in the input file.') +@click.option('--regexp_match', is_flag=True, + help='Use all metric that match given key. Can be used with stats and timeseries.') @click.option('--timeseries', type=str, multiple=True, help='Show a graph for given metric over time.') @click.option('--commulative_timeseries', type=str, multiple=True, @@ -35,6 +38,8 @@ import termtables 'between Physics Actors and physics_time_taken. Format: --plot .') @click.option('--stats', type=str, multiple=True, help='Print table with stats for a given metric containing min, max, mean, median etc.') +@click.option('--precision', type=int, + help='Format floating point numbers with given precision') @click.option('--timeseries_sum', is_flag=True, help='Add a graph to timeseries for a sum per frame of all given timeseries metrics.') @click.option('--commulative_timeseries_sum', is_flag=True, @@ -54,7 +59,7 @@ import termtables @click.option('--threshold_value', type=float, default=1.05/60, help='Threshold for hist_over.') @click.argument('path', type=click.Path(), nargs=-1) -def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats, +def main(print_keys, regexp_match, timeseries, hist, hist_ratio, stdev_hist, plot, stats, precision, timeseries_sum, stats_sum, begin_frame, end_frame, path, commulative_timeseries, commulative_timeseries_sum, frame_number_name, hist_threshold, threshold_name, threshold_value): @@ -68,10 +73,10 @@ def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats, for v in keys: print(v) if timeseries: - draw_timeseries(sources=frames, keys=timeseries, add_sum=timeseries_sum, + draw_timeseries(sources=frames, keys=matching_keys(keys, timeseries, regexp_match), add_sum=timeseries_sum, begin_frame=begin_frame, end_frame=end_frame) if commulative_timeseries: - draw_commulative_timeseries(sources=frames, keys=commulative_timeseries, add_sum=commulative_timeseries_sum, + draw_commulative_timeseries(sources=frames, keys=matching_keys(keys, commulative_timeseries, regexp_match), add_sum=commulative_timeseries_sum, begin_frame=begin_frame, end_frame=end_frame) if hist: draw_hists(sources=frames, keys=hist) @@ -82,7 +87,7 @@ def main(print_keys, timeseries, hist, hist_ratio, stdev_hist, plot, stats, if plot: draw_plots(sources=frames, plots=plot) if stats: - print_stats(sources=frames, keys=stats, stats_sum=stats_sum) + print_stats(sources=frames, keys=matching_keys(keys, stats, regexp_match), stats_sum=stats_sum, precision=precision) if hist_threshold: draw_hist_threshold(sources=frames, keys=hist_threshold, begin_frame=begin_frame, threshold_name=threshold_name, threshold_value=threshold_value) @@ -140,6 +145,12 @@ def collect_unique_keys(sources): return sorted(result) +def matching_keys(keys, patterns, regexp_match): + if regexp_match: + return { key for pattern in patterns for key in keys if re.search(pattern, key) } + return keys + + def draw_timeseries(sources, keys, add_sum, begin_frame, end_frame): fig, ax = matplotlib.pyplot.subplots() x = numpy.array(range(begin_frame, end_frame)) @@ -242,13 +253,13 @@ def draw_plots(sources, plots): fig.canvas.set_window_title('plots') -def print_stats(sources, keys, stats_sum): +def print_stats(sources, keys, stats_sum, precision): stats = list() for name, frames in sources.items(): for key in keys: - stats.append(make_stats(source=name, key=key, values=filter_not_none(frames[key]))) + stats.append(make_stats(source=name, key=key, values=filter_not_none(frames[key]), precision=precision)) if stats_sum: - stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys))) + stats.append(make_stats(source=name, key='sum', values=sum_multiple(frames, keys), precision=precision)) metrics = list(stats[0].keys()) termtables.print( [list(v.values()) for v in stats], @@ -282,6 +293,10 @@ def filter_not_none(values): return [v for v in values if v is not None] +def fixed_float(value, precision): + return '{v:.{p}f}'.format(v=value, p=precision) if precision else value + + def sum_multiple(frames, keys): result = collections.Counter() for key in keys: @@ -292,17 +307,17 @@ def sum_multiple(frames, keys): return numpy.array([result[k] for k in sorted(result.keys())]) -def make_stats(source, key, values): +def make_stats(source, key, values, precision): return collections.OrderedDict( source=source, key=key, number=len(values), - min=min(values), - max=max(values), - mean=statistics.mean(values), - median=statistics.median(values), - stdev=statistics.stdev(values), - q95=numpy.quantile(values, 0.95), + min=fixed_float(min(values), precision), + max=fixed_float(max(values), precision), + mean=fixed_float(statistics.mean(values), precision), + median=fixed_float(statistics.median(values), precision), + stdev=fixed_float(statistics.stdev(values), precision), + q95=fixed_float(numpy.quantile(values, 0.95), precision), )