diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000000..f9375a3ba5 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,50 @@ +name: CMake + +on: + pull_request: + branches: [ master ] + +env: + BUILD_TYPE: RelWithDebInfo + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Add OpenMW PPA Dependancies + run: sudo add-apt-repository ppa:openmw/openmw; sudo apt-get update + + - name: Install Building Dependancies + run: sudo CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic + + - name: Prime ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} + max-size: 1000M + + - name: 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 + + - name: Build + run: cmake --build . --config ${{env.BUILD_TYPE}} --parallel 3 + + - name: Install + shell: bash + run: cmake --install . + + - name: Create Artifact + shell: bash + working-directory: install + run: | + ls -laR + 7z a ../build_artifact.7z . + + - name: Upload Artifact + uses: actions/upload-artifact@v1 + with: + path: ./build_artifact.7z + name: build_artifact.7z diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ceba2841fe..c9b8cf9341 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,23 @@ stages: paths: - build/install/ +Clang_Tidy: + extends: .Debian_Image + stage: build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + before_script: + - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic clang-tidy clang + script: + - CI/before_script.linux.sh + - cd build + - cmake --build . -- -j $(nproc) openmw esmtool bsatool niftest openmw-wizard openmw-launcher openmw-iniimporter openmw-essimporter + variables: + CC: clang + CXX: clang++ + CI_CLANG_TIDY: 1 + timeout: 8h + Coverity: extends: .Debian_Image stage: build @@ -42,8 +59,8 @@ Coverity: - tar xfz /tmp/cov-analysis-linux64.tgz script: - CI/before_script.linux.sh - # Add more than just `openmw` once we can build everything under 3h - - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw + # Remove the specific targets and build everything once we can do it under 3h + - cov-analysis-linux64-*/bin/cov-build --dir cov-int cmake --build build -- -j $(nproc) openmw esmtool bsatool niftest openmw-wizard openmw-launcher openmw-iniimporter openmw-essimporter after_script: - tar cfz cov-int.tar.gz cov-int - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME @@ -53,7 +70,7 @@ Coverity: variables: CC: gcc CXX: g++ - timeout: 8h + artifacts: Debian_GCC: extends: .Debian @@ -76,6 +93,15 @@ Debian_GCC_tests: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 +Debian_GCC_tests_Debug: + extends: Debian_GCC + cache: + key: Debian_GCC_tests_Debug.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + Debian_GCC_Static_Deps: extends: Debian_GCC cache: @@ -88,6 +114,7 @@ Debian_GCC_Static_Deps: - 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 @@ -118,6 +145,15 @@ Debian_Clang_tests: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 +Debian_Clang_tests_Debug: + extends: Debian_Clang + cache: + key: Debian_Clang_tests_Debug.v1 + variables: + CCACHE_SIZE: 1G + BUILD_TESTS_ONLY: 1 + CMAKE_BUILD_TYPE: Debug + .MacOS: image: macos-11-xcode-12 tags: @@ -187,6 +223,18 @@ variables: &tests-targets - choco install ninja -y - choco install python -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: - $time = (Get-Date -Format "HH:mm:ss") @@ -198,12 +246,13 @@ variables: &tests-targets - cmake --build . --config $config --target ($targets.Split(',')) - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log @@ -289,6 +338,18 @@ Windows_Ninja_Tests_RelWithDebInfo: - choco install vswhere -y - choco install python -y - refreshenv + - | + function Make-SafeFileName { + param( + [Parameter(Mandatory=$true)] + [String] + $FileName + ) + [IO.Path]::GetInvalidFileNameChars() | ForEach-Object { + $FileName = $FileName.Replace($_, '_') + } + return $FileName + } stage: build script: - $time = (Get-Date -Format "HH:mm:ss") @@ -299,12 +360,13 @@ Windows_Ninja_Tests_RelWithDebInfo: - cmake --build . --config $config --target ($targets.Split(',')) - cd $config - echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt + - Get-ChildItem -Recurse *.ilk | Remove-Item - | if (Get-ChildItem -Recurse *.pdb) { - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt + 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt Get-ChildItem -Recurse *.pdb | Remove-Item } - - 7z a -tzip ..\..\OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip '*' + - 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${package}_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*' - if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } } after_script: - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log @@ -412,3 +474,4 @@ Debian_AndroidNDK_arm64-v8a: - build/install/ # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. timeout: 1h30m + diff --git a/AUTHORS.md b/AUTHORS.md index aa492f6c1e..2080f12a99 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -44,6 +44,7 @@ Programmers Austin Salgat (Salgat) Ben Shealy (bentsherman) Berulacks + Bo Svensson Britt Mathis (galdor557) Capostrophic Carl Maxwell @@ -79,6 +80,7 @@ Programmers Federico Guerra (FedeWar) Fil Krynicki (filkry) Finbar Crago (finbar-crago) + Florent Teppe (Tetramir) Florian Weber (Florianjw) Frédéric Chardon (fr3dz10) Gaëtan Dezeiraud (Brouilles) @@ -99,6 +101,7 @@ Programmers James Stephens (james-h-stephens) Jan-Peter Nilsson (peppe) Jan Borsodi (am0s) + JanuarySnow Jason Hooks (jhooks) jeaye jefetienne @@ -159,6 +162,7 @@ Programmers Nikolay Kasyanov (corristo) nobrakal Nolan Poe (nopoe) + Nurivan Gomez (Nuri-G) Oleg Chkan (mrcheko) Paul Cercueil (pcercuei) Paul McElroy (Greendogo) @@ -225,6 +229,7 @@ Programmers Yuri Krupenin zelurker Noah Gooder + Andrew Appuhamy (andrew-app) Documentation ------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd9bdbba4..4b7ef5b62a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,80 @@ 0.48.0 ------ + Bug #1751: Birthsign abilities increase modified attribute values instead of base ones + Bug #3246: ESSImporter: Most NPCs are dead on save load + 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 #3846: Strings starting with "-" fail to compile if not enclosed in quotes Bug #3905: Great House Dagoth issues + Bug #4203: Resurrecting an actor should close the loot GUI + Bug #4602: Robert's Bodies: crash inside createInstance() + Bug #4700: Editor: Incorrect command implementation + Bug #4744: Invisible particles must still be processed + 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 #5379: Wandering NPCs falling through cantons Bug #5453: Magic effect VFX are offset for creatures Bug #5483: AutoCalc flag is not used to calculate spells cost + Bug #5508: Engine binary links to Qt without using it + Bug #5596: Effects in constant spells should not be merged + Bug #5621: Drained stats cannot be restored + Bug #5755: Active grid object paging - disappearing textures + Bug #5766: Active grid object paging - disappearing textures + Bug #5788: Texture editing parses the selected indexes wrongly + Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention + Bug #5842: GetDisposition adds temporary disposition change from different actors + Bug #5863: GetEffect should return true after the player has teleported 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 Bug #6067: esp loader fails in for certain subrecord orders + Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed Bug #6115: Showmap overzealous matching + Bug #6118: Creature landing sound counts as a footstep Bug #6123: NPC with broken script freezes the game on hello Bug #6129: Player avatar not displayed correctly for large window sizes when GUI scaling active Bug #6131: Item selection in the avatar window not working correctly for large window sizes Bug #6133: Cannot reliably sneak or steal in the sight of the NPCs siding with player Bug #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 #6172: Some creatures can't open doors + Bug #6174: Spellmaking and Enchanting sliders differences from vanilla + Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla + Bug #6197: Infinite Casting Loop + Bug #6223: Some Constant Effect Bound Items inconsistencies + 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 #6289: Keyword search in dialogues expected the text to be all ASCII characters + Bug #6302: Teleporting disabled actor breaks its disabled state + Bug #6307: Pathfinding in Infidelities quest from Tribunal addon is broken + Feature #890: OpenMW-CS: Column filtering + 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 + Feature #4297: Implement APPLIED_ONCE flag for magic effects + Feature #4414: Handle duration of EXTRA SPELL magic effect + Feature #4595: Unique object identifier + Feature #4737: Handle instance move from one cell to another + Feature #5198: Implement "Magic effect expired" event + Feature #5454: Clear active spells from actor when he disappears from scene Feature #5489: MCP: Telekinesis fix for activators + Feature #5737: Handle instance move from one cell to another Feature #5996: Support Lua scripts in OpenMW Feature #6017: Separate persistent and temporary cell references when saving + Feature #6032: Reverse-z depth buffer + Feature #6162: Refactor GUI to use shaders and to be GLES and GL3+ friendly + Feature #6199: Support FBO Rendering + 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. + 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 ------ @@ -1930,6 +1982,7 @@ Bug #2025: Missing mouse-over text for non affordable items Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall" Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding + Bug #3066: Editor doesn't check if IDs and other strings are longer than their hardcoded field length Feature #471: Editor: Special case implementation for top-level window with single sub-window Feature #472: Editor: Sub-Window re-use settings Feature #704: Font colors import from fallback settings diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index d962e76086..ec4ece6343 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -1,11 +1,15 @@ #!/bin/sh -ex +export HOMEBREW_NO_EMOJI=1 +brew update --quiet + # workaround python issue on travis -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies qt@6 || true +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies python@3.8 || true +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies python@3.9 || true +[ -z "${TRAVIS}" ] && brew uninstall --ignore-dependencies qt@6 || true # Some of these tools can come from places other than brew, so check before installing +[ -z "${TRAVIS}" ] && brew install fontconfig command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake command -v qmake >/dev/null 2>&1 || brew install qt@5 @@ -15,10 +19,6 @@ ccache --version cmake --version qmake --version -brew install lua - -curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20210617.zip -o ~/openmw-deps.zip +curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20210716.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null -# additional libraries -[ -z "${TRAVIS}" ] && HOMEBREW_NO_AUTO_UPDATE=1 brew install fontconfig diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh index 17292e4e98..bc0eb0013d 100755 --- a/CI/before_script.linux.sh +++ b/CI/before_script.linux.sh @@ -14,6 +14,12 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then BUILD_BENCHMARKS=ON fi +CXX_FLAGS='-Werror -Wno-error=deprecated-declarations -Wno-error=nonnull -Wno-error=deprecated-copy' + +if [[ "${CXX}" == 'clang++' ]]; then + CXX_FLAGS="${CXX_FLAGS} -Wno-error=unused-lambda-capture -Wno-error=gnu-zero-variadic-macro-arguments" +fi + declare -a CMAKE_CONF_OPTS=( -DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}" -DCMAKE_CXX_COMPILER="${CXX:-/usr/bin/c++}" @@ -24,6 +30,8 @@ declare -a CMAKE_CONF_OPTS=( -DBUILD_SHARED_LIBS=OFF -DUSE_SYSTEM_TINYXML=ON -DCMAKE_INSTALL_PREFIX=install + -DCMAKE_C_FLAGS='-Werror' + -DCMAKE_CXX_FLAGS="${CXX_FLAGS}" ) if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then @@ -34,6 +42,19 @@ if [[ $CI_OPENMW_USE_STATIC_DEPS ]]; then ) fi +if [[ $CI_CLANG_TIDY ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_CXX_CLANG_TIDY='clang-tidy;-checks=-*,boost-*,clang-analyzer-*,concurrency-*,performance-*,-header-filter=.*,bugprone-*,misc-definitions-in-headers,misc-misplaced-const,misc-redundant-expression' + ) +fi + + +if [[ "${CMAKE_BUILD_TYPE}" ]]; then + CMAKE_CONF_OPTS+=( + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ) +fi + mkdir -p build cd build diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 27667c1c82..265e05b8ee 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -25,6 +25,5 @@ cmake \ -D BUILD_BSATOOL=TRUE \ -D BUILD_ESSIMPORTER=TRUE \ -D BUILD_NIFTEST=TRUE \ --D USE_LUAJIT=FALSE \ -G"Unix Makefiles" \ .. diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 6a7f4a84a7..4b47c937da 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -29,6 +29,7 @@ declare -rA GROUPED_DEPS=( # These dependencies can alternatively be built and linked statically. [openmw-deps-dynamic]="libmygui-dev libopenscenegraph-dev" [coverity]="curl" + [clang-tidy]="clang-tidy" # Pre-requisites for building MyGUI and OSG for static linking. # diff --git a/CMakeLists.txt b/CMakeLists.txt index d20e451676..1b8350a04b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,11 @@ if(POLICY CMP0083) cmake_policy(SET CMP0083 NEW) endif() +# to link with freetype library +if(POLICY CMP0079) + cmake_policy(SET CMP0079 NEW) +endif() + option(OPENMW_GL4ES_MANUAL_INIT "Manually initialize gl4es. This is more reliable on platforms without a windowing system. Requires gl4es to be configured with -DNOEGL=ON -DNO_LOADER=ON -DNO_INIT_CONSTRUCTOR=ON." OFF) if(OPENMW_GL4ES_MANUAL_INIT) add_definitions(-DOPENMW_GL4ES_MANUAL_INIT) @@ -202,8 +207,6 @@ if (USE_QT) find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) find_package(Qt5OpenGL REQUIRED) - # Instruct CMake to run moc automatically when needed. - #set(CMAKE_AUTOMOC ON) endif() set(USED_OSG_COMPONENTS @@ -404,11 +407,8 @@ else(USE_LUAJIT) add_compile_definitions(NO_LUAJIT) endif(USE_LUAJIT) -# Download sol - C++ library binding to Lua -file(DOWNLOAD - "https://github.com/ThePhD/sol2/releases/download/v3.2.2/sol.hpp" "${OpenMW_BINARY_DIR}/extern/sol3.2.2/sol/sol.hpp" - EXPECTED_MD5 ba113cf458f60672917108e69bb4d958) -set(SOL_INCLUDE_DIRS ${OpenMW_BINARY_DIR}/extern/sol3.2.2 ${OpenMW_SOURCE_DIR}/extern/sol_config) +# C++ library binding to Lua +set(SOL_INCLUDE_DIRS ${OpenMW_SOURCE_DIR}/extern/sol3.2.2 ${OpenMW_SOURCE_DIR}/extern/sol_config) include_directories( BEFORE SYSTEM @@ -515,7 +515,7 @@ endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long") + set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wundef -Wno-unused-parameter -pedantic -Wno-long-long ${CMAKE_CXX_FLAGS}") add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) if (APPLE) @@ -545,7 +545,6 @@ endif() # Components add_subdirectory (components) -target_compile_definitions(components PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") # Apps and tools if (BUILD_OPENMW) @@ -681,7 +680,7 @@ if (WIN32) if (BUILD_OPENMW) # \bigobj is required: # 1) for OPENMW_UNITY_BUILD; - # 2) to compile lua binginds, because sol3 is heavily templated. + # 2) to compile lua bindings, because sol3 is heavily templated. set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") endif() diff --git a/apps/benchmarks/detournavigator/navmeshtilescache.cpp b/apps/benchmarks/detournavigator/navmeshtilescache.cpp index d39161fda2..6c530db41c 100644 --- a/apps/benchmarks/detournavigator/navmeshtilescache.cpp +++ b/apps/benchmarks/detournavigator/navmeshtilescache.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -15,13 +16,12 @@ namespace osg::Vec3f mAgentHalfExtents; TilePosition mTilePosition; RecastMesh mRecastMesh; - std::vector mOffMeshConnections; }; struct Item { Key mKey; - NavMeshData mValue; + PreparedNavMeshData mValue; }; template @@ -31,6 +31,14 @@ namespace 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)}; + } + template osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random) { @@ -81,22 +89,55 @@ namespace template void generateWater(OutputIterator out, std::size_t count, Random& random) { - std::uniform_real_distribution distribution(0.0, 1.0); + std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { - const btVector3 shift(distribution(random), distribution(random), distribution(random)); - return RecastMesh::Water {1, btTransform(btMatrix3x3::getIdentity(), shift)}; + const osg::Vec3f shift(distribution(random), distribution(random), distribution(random)); + return Cell {1, shift}; }); } - template - void generateOffMeshConnection(OutputIterator out, std::size_t count, Random& random) + template + Mesh generateMesh(std::size_t triangles, Random& random) { - std::uniform_real_distribution distribution(0.0, 1.0); - std::generate_n(out, count, [&] { - const osg::Vec3f start(distribution(random), distribution(random), distribution(random)); - const osg::Vec3f end(distribution(random), distribution(random), distribution(random)); - return OffMeshConnection {start, end, generateAreaType(random)}; + std::uniform_real_distribution distribution(0.0, 1.0); + std::vector vertices; + std::vector indices; + std::vector areaTypes; + if (distribution(random) < 0.939) + { + generateVertices(std::back_inserter(vertices), triangles * 2.467, random); + generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.279, random); + generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); + } + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + + template + Heightfield generateHeightfield(Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + Heightfield result; + result.mBounds = generateTileBounds(random); + 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); }); + return result; + } + + template + FlatHeightfield generateFlatHeightfield(Random& random) + { + std::uniform_real_distribution distribution(0.0, 1.0); + FlatHeightfield result; + result.mBounds = generateTileBounds(random); + result.mHeight = distribution(random); + return result; } template @@ -106,22 +147,15 @@ namespace const TilePosition tilePosition = generateTilePosition(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); - std::vector vertices; - generateVertices(std::back_inserter(vertices), triangles * 1.98, random); - std::vector indices; - generateIndices(std::back_inserter(indices), static_cast(vertices.size() / 3) - 1, vertices.size() * 1.53, random); - std::vector areaTypes; - generateAreaTypes(std::back_inserter(areaTypes), indices.size() / 3, random); - std::vector water; - generateWater(std::back_inserter(water), 2, random); - RecastMesh recastMesh(generation, revision, std::move(indices), std::move(vertices), - std::move(areaTypes), std::move(water)); - std::vector offMeshConnections; - generateOffMeshConnection(std::back_inserter(offMeshConnections), 300, random); - return Key {agentHalfExtents, tilePosition, std::move(recastMesh), std::move(offMeshConnections)}; + Mesh mesh = generateMesh(triangles, random); + std::vector water; + generateWater(std::back_inserter(water), 1, random); + RecastMesh recastMesh(generation, revision, std::move(mesh), std::move(water), + {generateHeightfield(random)}, {generateFlatHeightfield(random)}); + return Key {agentHalfExtents, tilePosition, std::move(recastMesh)}; } - constexpr std::size_t trianglesPerTile = 310; + constexpr std::size_t trianglesPerTile = 239; template void generateKeys(OutputIterator out, std::size_t count, Random& random) @@ -137,7 +171,8 @@ namespace while (true) { Key key = generateKey(trianglesPerTile, random); - cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, + std::make_unique()); *out++ = std::move(key); const std::size_t newSize = cache.getStats().mNavMeshCacheSize; if (size >= newSize) @@ -159,7 +194,7 @@ namespace while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections); + const auto result = cache.get(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh); benchmark::DoNotOptimize(result); } } @@ -187,7 +222,8 @@ namespace while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; - const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, key.mOffMeshConnections, NavMeshData()); + const auto result = cache.set(key.mAgentHalfExtents, key.mTilePosition, key.mRecastMesh, + std::make_unique()); benchmark::DoNotOptimize(result); } } diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 6b6e428e62..bf5f47a76d 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -236,7 +236,9 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) if(!quiet) std::cout << " References:\n"; bool deleted = false; - while(cell.getNextRef(esm, ref, deleted)) + ESM::MovedCellRef movedCellRef; + bool moved = false; + while(cell.getNextRef(esm, ref, deleted, movedCellRef, moved)) { if (save) { info.data.mCellRefs[&cell].push_back(std::make_pair(ref, deleted)); @@ -244,7 +246,7 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) if(quiet) continue; - std::cout << " Refnum: " << ref.mRefNum.mIndex << '\n'; + std::cout << " - Refnum: " << ref.mRefNum.mIndex << '\n'; std::cout << " ID: " << ref.mRefID << '\n'; std::cout << " Position: (" << ref.mPos.pos[0] << ", " << ref.mPos.pos[1] << ", " << ref.mPos.pos[2] << ")\n"; if (ref.mScale != 1.f) @@ -276,6 +278,13 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info) if (!ref.mDestCell.empty()) std::cout << " Destination cell: " << ref.mDestCell << '\n'; } + std::cout << " Moved: " << std::boolalpha << moved << std::noboolalpha << '\n'; + if (moved) + { + std::cout << " Moved refnum: " << movedCellRef.mRefNum.mIndex << '\n'; + std::cout << " Moved content file: " << movedCellRef.mRefNum.mContentFile << '\n'; + std::cout << " Target: " << movedCellRef.mTarget[0] << ", " << movedCellRef.mTarget[1] << '\n'; + } } } diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp index e0756602dd..874b936baf 100644 --- a/apps/essimporter/converter.cpp +++ b/apps/essimporter/converter.cpp @@ -2,6 +2,7 @@ #include #include +#include // INT_MIN #include @@ -357,7 +358,7 @@ namespace ESSImport std::string idLower = Misc::StringUtils::lowerCase(out.mRefID); - std::map, NPCC>::const_iterator npccIt = mContext->mNpcChanges.find( + auto npccIt = mContext->mNpcChanges.find( std::make_pair(refIndex, out.mRefID)); if (npccIt != mContext->mNpcChanges.end()) { @@ -369,6 +370,8 @@ namespace ESSImport // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mGoldPool = INT_MIN; // HACK: indicates no ACDT if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertNpcData(cellref, objstate.mNpcStats); @@ -383,7 +386,7 @@ namespace ESSImport continue; } - std::map, CNTC>::const_iterator cntcIt = mContext->mContainerChanges.find( + auto cntcIt = mContext->mContainerChanges.find( std::make_pair(refIndex, out.mRefID)); if (cntcIt != mContext->mContainerChanges.end()) { @@ -398,7 +401,7 @@ namespace ESSImport continue; } - std::map, CREC>::const_iterator crecIt = mContext->mCreatureChanges.find( + auto crecIt = mContext->mCreatureChanges.find( std::make_pair(refIndex, out.mRefID)); if (crecIt != mContext->mCreatureChanges.end()) { @@ -410,6 +413,8 @@ namespace ESSImport // from the ESM with default values if (cellref.mHasACDT) convertACDT(cellref.mACDT, objstate.mCreatureStats); + else + objstate.mCreatureStats.mGoldPool = INT_MIN; // HACK: indicates no ACDT if (cellref.mHasACSC) convertACSC(cellref.mACSC, objstate.mCreatureStats); convertCREC(crecIt->second, objstate); diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 9a1923c2b6..81b9711bbf 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -124,11 +124,9 @@ public: { mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel; mContext->mPlayerBase = npc; - ESM::SpellState::SpellParams empty; // FIXME: player start spells and birthsign spells aren't listed here, // need to fix openmw to account for this - for (const auto & spell : npc.mSpells.mList) - mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty; + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList; // Clear the list now that we've written it, this prevents issues cropping up with // ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal. @@ -374,7 +372,7 @@ public: void write(ESM::ESMWriter &esm) override { esm.startRecord(ESM::REC_DCOU); - for (std::map::const_iterator it = mKillCounter.begin(); it != mKillCounter.end(); ++it) + for (auto it = mKillCounter.begin(); it != mKillCounter.end(); ++it) { esm.writeHNString("ID__", it->first); esm.writeHNT ("COUN", it->second); @@ -397,7 +395,7 @@ public: faction.load(esm, isDeleted); std::string id = Misc::StringUtils::lowerCase(faction.mId); - for (std::map::const_iterator it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) + for (auto it = faction.mReactions.begin(); it != faction.mReactions.end(); ++it) { std::string faction2 = Misc::StringUtils::lowerCase(it->first); mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); @@ -431,7 +429,7 @@ public: void write(ESM::ESMWriter &esm) override { ESM::StolenItems items; - for (std::map >::const_iterator it = mStolenItems.begin(); it != mStolenItems.end(); ++it) + for (auto it = mStolenItems.begin(); it != mStolenItems.end(); ++it) { std::map, int> owners; for (const auto & ownerIt : it->second) @@ -487,7 +485,7 @@ public: } void write(ESM::ESMWriter &esm) override { - for (std::map::const_iterator it = mDials.begin(); it != mDials.end(); ++it) + for (auto it = mDials.begin(); it != mDials.end(); ++it) { esm.startRecord(ESM::REC_QUES); ESM::QuestState state; diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 706512263e..27f77e40d1 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -385,7 +385,7 @@ namespace ESSImport // Writing order should be Dynamic Store -> Cells -> Player, // so that references to dynamic records can be recognized when loading - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 0) @@ -398,7 +398,7 @@ namespace ESSImport context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_); - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 1) @@ -423,7 +423,7 @@ namespace ESSImport writer.endRecord(ESM::REC_ACTC); // Stage 2 requires cell references to be written / actors IDs assigned - for (std::map >::const_iterator it = converters.begin(); + for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 2) diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index 3018237705..7e8bd67e94 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -36,23 +36,6 @@ set(LAUNCHER_HEADER ) # Headers that must be pre-processed -set(LAUNCHER_HEADER_MOC - datafilespage.hpp - graphicspage.hpp - maindialog.hpp - playpage.hpp - textslotmsgbox.hpp - settingspage.hpp - advancedpage.hpp - - utils/cellnameloader.hpp - utils/textinputdialog.hpp - utils/profilescombobox.hpp - utils/lineedit.hpp - utils/openalutil.hpp - -) - set(LAUNCHER_UI ${CMAKE_SOURCE_DIR}/files/ui/datafilespage.ui ${CMAKE_SOURCE_DIR}/files/ui/graphicspage.ui @@ -74,7 +57,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc) -QT5_WRAP_CPP(MOC_SRCS ${LAUNCHER_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${LAUNCHER_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -99,7 +81,7 @@ endif (WIN32) target_link_libraries(openmw-launcher ${SDL2_LIBRARY_ONLY} ${OPENAL_LIBRARY} - components + components_qt ) target_link_libraries(openmw-launcher Qt5::Widgets Qt5::Core) @@ -109,4 +91,7 @@ if (BUILD_WITH_CODE_COVERAGE) target_link_libraries(openmw-launcher gcov) endif() +if(USE_QT) + set_property(TARGET openmw-launcher PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 00021268e3..2c33992ac3 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -192,6 +192,7 @@ bool Launcher::AdvancedPage::loadSettings() if (showOwnedIndex >= 0 && showOwnedIndex <= 3) showOwnedComboBox->setCurrentIndex(showOwnedIndex); loadSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + loadSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map"); loadSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); scalingSpinBox->setValue(Settings::Manager::getFloat("scaling factor", "GUI")); } @@ -352,6 +353,7 @@ void Launcher::AdvancedPage::saveSettings() if (showOwnedCurrentIndex != Settings::Manager::getInt("show owned", "Game")) Settings::Manager::setInt("show owned", "Game", showOwnedCurrentIndex); saveSettingBool(stretchBackgroundCheckBox, "stretch menu background", "GUI"); + saveSettingBool(useZoomOnMapCheckBox, "allow zooming", "Map"); saveSettingBool(graphicHerbalismCheckBox, "graphic herbalism", "Game"); float uiScalingFactor = scalingSpinBox->value(); if (uiScalingFactor != Settings::Manager::getFloat("scaling factor", "GUI")) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 956483a3f2..24729d5096 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -17,7 +17,6 @@ #include #include -#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index ebb031e9ef..8359834ddb 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -14,8 +14,7 @@ #include #include - -#include +#include QString getAspect(int x, int y) { diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index d41cd529d3..75d4464b68 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -52,8 +51,8 @@ Launcher::MainDialog::MainDialog(QWidget *parent) iconWidget->setCurrentRow(0); iconWidget->setFlow(QListView::LeftToRight); - QPushButton *helpButton = new QPushButton(tr("Help")); - QPushButton *playButton = new QPushButton(tr("Play")); + auto *helpButton = new QPushButton(tr("Help")); + auto *playButton = new QPushButton(tr("Play")); buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); buttonBox->addButton(helpButton, QDialogButtonBox::HelpRole); buttonBox->addButton(playButton, QDialogButtonBox::AcceptRole); @@ -79,31 +78,31 @@ void Launcher::MainDialog::createIcons() if (!QIcon::hasThemeIcon("document-new")) QIcon::setThemeName("tango"); - QListWidgetItem *playButton = new QListWidgetItem(iconWidget); + auto *playButton = new QListWidgetItem(iconWidget); playButton->setIcon(QIcon(":/images/openmw.png")); playButton->setText(tr("Play")); playButton->setTextAlignment(Qt::AlignCenter); playButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *dataFilesButton = new QListWidgetItem(iconWidget); + auto *dataFilesButton = new QListWidgetItem(iconWidget); dataFilesButton->setIcon(QIcon(":/images/openmw-plugin.png")); dataFilesButton->setText(tr("Data Files")); dataFilesButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); dataFilesButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *graphicsButton = new QListWidgetItem(iconWidget); + auto *graphicsButton = new QListWidgetItem(iconWidget); graphicsButton->setIcon(QIcon(":/images/preferences-video.png")); graphicsButton->setText(tr("Graphics")); graphicsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom | Qt::AlignAbsolute); graphicsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *settingsButton = new QListWidgetItem(iconWidget); + auto *settingsButton = new QListWidgetItem(iconWidget); settingsButton->setIcon(QIcon(":/images/preferences.png")); settingsButton->setText(tr("Settings")); settingsButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); settingsButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); - QListWidgetItem *advancedButton = new QListWidgetItem(iconWidget); + auto *advancedButton = new QListWidgetItem(iconWidget); advancedButton->setIcon(QIcon(":/images/preferences-advanced.png")); advancedButton->setText(tr("Advanced")); advancedButton->setTextAlignment(Qt::AlignHCenter | Qt::AlignBottom); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index ca7fd028a7..9c750a7c25 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -4,11 +4,6 @@ #include #include -#include - -#include -#include - #include "utils/textinputdialog.hpp" #include "datafilespage.hpp" diff --git a/apps/launcher/utils/openalutil.cpp b/apps/launcher/utils/openalutil.cpp index 52ad20894d..53fd704203 100644 --- a/apps/launcher/utils/openalutil.cpp +++ b/apps/launcher/utils/openalutil.cpp @@ -1,6 +1,5 @@ #include #include -#include #include diff --git a/apps/launcher/utils/profilescombobox.cpp b/apps/launcher/utils/profilescombobox.cpp index af349ddfff..d06cfacff6 100644 --- a/apps/launcher/utils/profilescombobox.cpp +++ b/apps/launcher/utils/profilescombobox.cpp @@ -29,7 +29,7 @@ void ProfilesComboBox::setEditEnabled(bool editable) setEditable(true); setValidator(mValidator); - ComboBoxLineEdit *edit = new ComboBoxLineEdit(this); + auto *edit = new ComboBoxLineEdit(this); setLineEdit(edit); setCompleter(nullptr); diff --git a/apps/launcher/utils/textinputdialog.cpp b/apps/launcher/utils/textinputdialog.cpp index 70b827596e..5bbcf4c198 100644 --- a/apps/launcher/utils/textinputdialog.cpp +++ b/apps/launcher/utils/textinputdialog.cpp @@ -16,7 +16,7 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mButtonBox->addButton(QDialogButtonBox::Cancel); mButtonBox->button(QDialogButtonBox::Ok)->setEnabled (false); - QLabel *label = new QLabel(this); + auto *label = new QLabel(this); label->setText(text); // Line edit @@ -25,7 +25,7 @@ Launcher::TextInputDialog::TextInputDialog(const QString& title, const QString & mLineEdit->setValidator(validator); mLineEdit->setCompleter(nullptr); - QVBoxLayout *dialogLayout = new QVBoxLayout(this); + auto *dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); dialogLayout->addWidget(mButtonBox); diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 2763d8ad95..35a1c4ec8b 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -778,7 +778,7 @@ void MwIniImporter::mergeFallback(multistrmap &cfg, const multistrmap &ini) cons } void MwIniImporter::insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value) { - const multistrmap::const_iterator it = cfg.find(key); + const auto it = cfg.find(key); if(it == cfg.end()) { cfg.insert(std::make_pair (key, std::vector() )); } @@ -791,7 +791,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con std::string archive; // Search archives listed in ini file - multistrmap::const_iterator it = ini.begin(); + auto it = ini.begin(); for(int i=0; it != ini.end(); i++) { archive = baseArchive; archive.append(std::to_string(i)); @@ -813,7 +813,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con // does not appears in the ini file cfg["fallback-archive"].push_back("Morrowind.bsa"); - for(std::vector::const_iterator iter=archives.begin(); iter!=archives.end(); ++iter) { + for(auto iter=archives.begin(); iter!=archives.end(); ++iter) { cfg["fallback-archive"].push_back(*iter); } } @@ -886,7 +886,7 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); - multistrmap::const_iterator it = ini.begin(); + auto it = ini.begin(); for (int i=0; it != ini.end(); i++) { std::string gameFile = baseGameFile; @@ -969,7 +969,7 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) { for(multistrmap::const_iterator it=cfg.begin(); it != cfg.end(); ++it) { - for(std::vector::const_iterator entry=it->second.begin(); entry != it->second.end(); ++entry) { + for(auto entry=it->second.begin(); entry != it->second.end(); ++entry) { out << (it->first) << "=" << (*entry) << std::endl; } } diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index e403562d32..cb6205ef5c 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -52,11 +52,8 @@ void readVFS(VFS::Archive* anArchive,std::string archivePath = "") myManager.addArchive(anArchive); myManager.buildIndex(); - std::map files=myManager.getIndex(); - for(std::map::const_iterator it=files.begin(); it!=files.end(); ++it) + for(const auto& name : myManager.getRecursiveDirectoryIterator("")) { - std::string name = it->first; - try{ if(isNIF(name)) { @@ -134,7 +131,7 @@ int main(int argc, char **argv) Nif::NIFFile::setLoadUnsupportedFiles(true); // std::cout << "Reading Files" << std::endl; - for(std::vector::const_iterator it=files.begin(); it!=files.end(); ++it) + for(auto it=files.begin(); it!=files.end(); ++it) { std::string name = *it; diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 88c4233c9c..952bbbdbda 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -8,29 +8,29 @@ opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) -opencs_units_noqt (model/doc +opencs_units (model/doc stage savingstate savingstages blacklist messages ) -opencs_hdrs_noqt (model/doc +opencs_hdrs (model/doc state ) opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel - actoradapter + actoradapter idcollection ) -opencs_units_noqt (model/world +opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection idcompletionmanager metadata defaultgmsts infoselectwrapper commandmacro ) -opencs_hdrs_noqt (model/world +opencs_hdrs (model/world columnimp idcollection collection info subcellcollection ) @@ -39,14 +39,14 @@ opencs_units (model/tools tools reportmodel mergeoperation ) -opencs_units_noqt (model/tools +opencs_units (model/tools mandatoryid skillcheck classcheck factioncheck racecheck soundcheck regioncheck birthsigncheck spellcheck referencecheck referenceablecheck scriptcheck bodypartcheck startscriptcheck search searchoperation searchstage pathgridcheck soundgencheck magiceffectcheck mergestages gmstcheck topicinfocheck journalcheck enchantmentcheck ) -opencs_hdrs_noqt (model/tools +opencs_hdrs (model/tools mergestate ) @@ -57,11 +57,11 @@ opencs_units (view/doc ) -opencs_units_noqt (view/doc +opencs_units (view/doc subviewfactory ) -opencs_hdrs_noqt (view/doc +opencs_hdrs (view/doc subviewfactoryimp ) @@ -71,10 +71,10 @@ opencs_units (view/world cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator - bodypartcreator landtexturecreator landcreator + bodypartcreator landtexturecreator landcreator tableheadermouseeventhandler ) -opencs_units_noqt (view/world +opencs_units (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils @@ -92,12 +92,12 @@ opencs_units (view/render cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) -opencs_units_noqt (view/render +opencs_units (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) -opencs_hdrs_noqt (view/render +opencs_hdrs (view/render mask ) @@ -106,7 +106,7 @@ opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) -opencs_units_noqt (view/tools +opencs_units (view/tools subviews ) @@ -119,11 +119,11 @@ opencs_units (model/prefs shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting ) -opencs_units_noqt (model/prefs +opencs_units (model/prefs category ) -opencs_units_noqt (model/filter +opencs_units (model/filter node unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) @@ -150,7 +150,6 @@ if(WIN32) endif(WIN32) qt5_wrap_ui(OPENCS_UI_HDR ${OPENCS_UI}) -qt5_wrap_cpp(OPENCS_MOC_SRC ${OPENCS_HDR_QT}) qt5_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) # for compiled .ui files @@ -229,34 +228,9 @@ target_link_libraries(openmw-cs ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} - components + components_qt ) -if(OSG_STATIC) - unset(_osg_plugins_static_files) - add_library(openmw_cs_osg_plugins INTERFACE) - foreach(_plugin ${USED_OSG_PLUGINS}) - string(TOUPPER ${_plugin} _plugin_uc) - if(OPENMW_USE_SYSTEM_OSG) - list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) - else() - list(APPEND _osg_plugins_static_files $) - target_link_libraries(openmw_cs_osg_plugins INTERFACE $) - add_dependencies(openmw_cs_osg_plugins ${${_plugin_uc}_LIBRARY}) - endif() - endforeach() - # We use --whole-archive because OSG plugins use registration. - get_whole_archive_options(_opts ${_osg_plugins_static_files}) - target_link_options(openmw_cs_osg_plugins INTERFACE ${_opts}) - target_link_libraries(openmw-cs openmw_cs_osg_plugins) - - if(OPENMW_USE_SYSTEM_OSG) - # OSG plugin pkgconfig files are missing these dependencies. - # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(openmw freetype jpeg png) - endif() -endif(OSG_STATIC) - target_link_libraries(openmw-cs Qt5::Widgets Qt5::Core Qt5::Network Qt5::OpenGL) if (WIN32) @@ -284,3 +258,7 @@ endif (MSVC) if(APPLE) INSTALL(TARGETS openmw-cs BUNDLE DESTINATION "." COMPONENT Bundle) endif() + +if(USE_QT) + set_property(TARGET openmw-cs PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 3a20555d1e..f4071c525d 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -1,6 +1,7 @@ #include "document.hpp" #include +#include #include #include @@ -115,10 +116,10 @@ void CSMDoc::Document::addOptionalGmst (const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId (gmst.mId)==-1) { - CSMWorld::Record record; - record.mBase = gmst; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGmsts().appendRecord (record); + std::unique_ptr > record(new CSMWorld::Record); + record->mBase = gmst; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGmsts().appendRecord (std::move(record)); } } @@ -126,10 +127,10 @@ void CSMDoc::Document::addOptionalGlobal (const ESM::Global& global) { if (getData().getGlobals().searchId (global.mId)==-1) { - CSMWorld::Record record; - record.mBase = global; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getGlobals().appendRecord (record); + std::unique_ptr > record(new CSMWorld::Record); + record->mBase = global; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getGlobals().appendRecord (std::move(record)); } } @@ -137,10 +138,10 @@ void CSMDoc::Document::addOptionalMagicEffect (const ESM::MagicEffect& magicEffe { if (getData().getMagicEffects().searchId (magicEffect.mId)==-1) { - CSMWorld::Record record; - record.mBase = magicEffect; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - getData().getMagicEffects().appendRecord (record); + std::unique_ptr > record(new CSMWorld::Record); + record->mBase = magicEffect; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + getData().getMagicEffects().appendRecord (std::move(record)); } } diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index 1c5a7348c8..9d2f89b8e9 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -85,19 +85,19 @@ void CSMDoc::Loader::load() return; } - if (iter->second.mFilesecond.mFilegetContentFiles()[iter->second.mFile]; - int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, false); + int steps = document->getData().startLoading (path, iter->second.mFile!=editedIndex, /*project*/false); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage (document, path.filename().string(), steps); } - else if (iter->second.mFile==size) + else if (iter->second.mFile==size) // start loading the last (project) file { - int steps = document->getData().startLoading (document->getProjectPath(), false, true); + int steps = document->getData().startLoading (document->getProjectPath(), /*base*/false, true); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp index 8dafbaf5ae..f1179b1624 100644 --- a/apps/opencs/model/doc/runner.cpp +++ b/apps/opencs/model/doc/runner.cpp @@ -83,6 +83,8 @@ void CSMDoc::Runner::start (bool delayed) arguments << QString::fromUtf8 (("--data=\""+mProjectPath.parent_path().string()+"\"").c_str()); + arguments << "--replace=content"; + for (std::vector::const_iterator iter (mContentFiles.begin()); iter!=mContentFiles.end(); ++iter) { diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index 8c17a3b8c1..a410d34b2a 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -114,7 +114,7 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) { infoModified = true; break; @@ -140,9 +140,9 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message // write modified selected info records for (CSMWorld::InfoCollection::RecordConstIterator iter (range.first); iter!=range.second; ++iter) { - if (iter->isModified() || iter->mState == CSMWorld::RecordBase::State_Deleted) + if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) { - ESM::DialInfo info = iter->get(); + ESM::DialInfo info = (*iter)->get(); info.mId = info.mId.substr (info.mId.find_last_of ('#')+1); info.mPrev = ""; @@ -151,7 +151,7 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message CSMWorld::InfoCollection::RecordConstIterator prev = iter; --prev; - info.mPrev = prev->get().mId.substr (prev->get().mId.find_last_of ('#')+1); + info.mPrev = (*prev)->get().mId.substr ((*prev)->get().mId.find_last_of ('#')+1); } CSMWorld::InfoCollection::RecordConstIterator next = iter; @@ -160,11 +160,11 @@ void CSMDoc::WriteDialogueCollectionStage::perform (int stage, Messages& message info.mNext = ""; if (next!=range.second) { - info.mNext = next->get().mId.substr (next->get().mId.find_last_of ('#')+1); + info.mNext = (*next)->get().mId.substr ((*next)->get().mId.find_last_of ('#')+1); } writer.startRecord (info.sRecordId); - info.save (writer, iter->mState == CSMWorld::RecordBase::State_Deleted); + info.save (writer, (*iter)->mState == CSMWorld::RecordBase::State_Deleted); writer.endRecord (info.sRecordId); } } diff --git a/apps/opencs/model/doc/savingstages.hpp b/apps/opencs/model/doc/savingstages.hpp index d59a1efe5e..9ccd1772e1 100644 --- a/apps/opencs/model/doc/savingstages.hpp +++ b/apps/opencs/model/doc/savingstages.hpp @@ -108,7 +108,7 @@ namespace CSMDoc state == CSMWorld::RecordBase::State_ModifiedOnly || state == CSMWorld::RecordBase::State_Deleted) { - writer.startRecord (record.sRecordId); + writer.startRecord (record.sRecordId, record.mRecordFlags); record.save (writer, state == CSMWorld::RecordBase::State_Deleted); writer.endRecord (record.sRecordId); } diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index ae83abfa01..5959cdf54b 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -35,7 +35,7 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it) { - const CSMWorld::Record infoRecord = (*it); + const CSMWorld::Record infoRecord = (*it->get()); if (infoRecord.isDeleted()) continue; diff --git a/apps/opencs/model/tools/mergestages.cpp b/apps/opencs/model/tools/mergestages.cpp index 016e2da392..021cf61d81 100644 --- a/apps/opencs/model/tools/mergestages.cpp +++ b/apps/opencs/model/tools/mergestages.cpp @@ -103,10 +103,9 @@ void CSMTools::MergeReferencesStage::perform (int stage, CSMDoc::Messages& messa ref.mRefNum.mContentFile = 0; ref.mNew = false; - CSMWorld::Record newRecord ( - CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref); - - mState.mTarget->getData().getReferences().appendRecord (newRecord); + mState.mTarget->getData().getReferences().appendRecord ( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref))); } } @@ -128,7 +127,9 @@ void CSMTools::PopulateLandTexturesMergeStage::perform (int stage, CSMDoc::Messa if (!record.isDeleted()) { - mState.mTarget->getData().getLandTextures().appendRecord(record); + mState.mTarget->getData().getLandTextures().appendRecord( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } @@ -150,7 +151,9 @@ void CSMTools::MergeLandStage::perform (int stage, CSMDoc::Messages& messages) if (!record.isDeleted()) { - mState.mTarget->getData().getLand().appendRecord (record); + mState.mTarget->getData().getLand().appendRecord ( + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } @@ -185,10 +188,9 @@ void CSMTools::FixLandsAndLandTexturesMergeStage::perform (int stage, CSMDoc::Me const CSMWorld::Record& oldRecord = mState.mTarget->getData().getLand().getRecord (stage); - CSMWorld::Record newRecord(CSMWorld::RecordBase::State_ModifiedOnly, - nullptr, &oldRecord.get()); - - mState.mTarget->getData().getLand().setRecord(stage, newRecord); + mState.mTarget->getData().getLand().setRecord (stage, + std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &oldRecord.get()))); } } diff --git a/apps/opencs/model/tools/mergestages.hpp b/apps/opencs/model/tools/mergestages.hpp index a6b6de58f8..778bea7c68 100644 --- a/apps/opencs/model/tools/mergestages.hpp +++ b/apps/opencs/model/tools/mergestages.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -82,7 +83,8 @@ namespace CSMTools const CSMWorld::Record& record = source.getRecord (stage); if (!record.isDeleted()) - target.appendRecord (CSMWorld::Record (CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get())); + target.appendRecord (std::make_unique >( + CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } class MergeRefIdsStage : public CSMDoc::Stage diff --git a/apps/opencs/model/world/actoradapter.cpp b/apps/opencs/model/world/actoradapter.cpp index 8558aa9bc9..7e7f926384 100644 --- a/apps/opencs/model/world/actoradapter.cpp +++ b/apps/opencs/model/world/actoradapter.cpp @@ -9,6 +9,9 @@ #include "data.hpp" +#include +#include + namespace CSMWorld { const std::string& ActorAdapter::RaceData::getId() const @@ -121,7 +124,7 @@ namespace CSMWorld return SceneUtil::getActorSkeleton(firstPerson, mFemale, beast, werewolf); } - const std::string ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const + std::string_view ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const { auto it = mParts.find(index); if (it == mParts.end()) @@ -131,7 +134,7 @@ namespace CSMWorld if (mFemale) { // Note: we should use male parts for females as fallback - const std::string femalePart = mRaceData->getFemalePart(index); + const std::string& femalePart = mRaceData->getFemalePart(index); if (!femalePart.empty()) return femalePart; } @@ -139,11 +142,10 @@ namespace CSMWorld return mRaceData->getMalePart(index); } - return ""; + return {}; } - const std::string& partName = it->second.first; - return partName; + return it->second.first; } bool ActorAdapter::ActorData::hasDependency(const std::string& id) const diff --git a/apps/opencs/model/world/actoradapter.hpp b/apps/opencs/model/world/actoradapter.hpp index 912a6bcb38..826e3b9179 100644 --- a/apps/opencs/model/world/actoradapter.hpp +++ b/apps/opencs/model/world/actoradapter.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -93,7 +95,7 @@ namespace CSMWorld /// Returns the skeleton the actor should use for attaching parts to std::string getSkeleton() const; /// Retrieves the associated actor part - const std::string getPart(ESM::PartReferenceType index) const; + std::string_view getPart(ESM::PartReferenceType index) const; /// Checks if the actor has a data dependency bool hasDependency(const std::string& id) const; diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 451ef9d0e1..6ab9d7ff9d 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include @@ -84,7 +86,7 @@ namespace CSMWorld private: - std::vector > mRecords; + std::vector > > mRecords; std::map mIndex; std::vector *> mColumns; @@ -94,9 +96,7 @@ namespace CSMWorld protected: - const std::map& getIdMap() const; - - const std::vector >& getRecords() const; + const std::vector > >& getRecords() const; bool reorderRowsImp (int baseIndex, const std::vector& newOrder); ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices @@ -154,16 +154,16 @@ namespace CSMWorld ///< Change the state of a record from base to modified, if it is not already. /// \return True if the record was changed. - int searchId (const std::string& id) const override; + int searchId(std::string_view id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - void replace (int index, const RecordBase& record) override; + void replace (int index, std::unique_ptr record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. - void appendRecord (const RecordBase& record, + void appendRecord (std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) override; ///< If the record type does not match, an exception is thrown. ///< \param type Will be ignored, unless the collection supports multiple record types @@ -181,7 +181,7 @@ namespace CSMWorld /// /// \param listDeleted include deleted record in the list - virtual void insertRecord (const RecordBase& record, int index, + virtual void insertRecord (std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None); ///< Insert record before index. /// @@ -198,20 +198,14 @@ namespace CSMWorld void addColumn (Column *column); - void setRecord (int index, const Record& record); + void setRecord (int index, std::unique_ptr > record); ///< \attention This function must not change the ID. NestableColumn *getNestableColumn (int column) const; }; template - const std::map& Collection::getIdMap() const - { - return mIndex; - } - - template - const std::vector >& Collection::getRecords() const + const std::vector > >& Collection::getRecords() const { return mRecords; } @@ -231,15 +225,15 @@ namespace CSMWorld return false; // reorder records - std::vector > buffer (size); + std::vector > > buffer (size); for (int i=0; isetModified (buffer[newOrder[i]]->get()); } - std::copy (buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); + std::move (buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); // adjust index for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); @@ -255,18 +249,18 @@ namespace CSMWorld int Collection::cloneRecordImp(const std::string& origin, const std::string& destination, UniversalId::Type type) { - Record copy; - copy.mModified = getRecord(origin).get(); - copy.mState = RecordBase::State_ModifiedOnly; - IdAccessorT().setId(copy.get(), destination); + std::unique_ptr > copy(new Record); + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + IdAccessorT().setId(copy->get(), destination); if (type == UniversalId::Type_Reference) { - CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©.mModified; + CSMWorld::CellRef* ptr = (CSMWorld::CellRef*) ©->mModified; ptr->mRefNum.mIndex = 0; } int index = getAppendIndex(destination, type); - insertRecord(copy, getAppendIndex(destination, type)); + insertRecord(std::move(copy), getAppendIndex(destination, type)); return index; } @@ -275,7 +269,7 @@ namespace CSMWorld int Collection::touchRecordImp(const std::string& id) { int index = getIndex(id); - Record& record = mRecords.at(index); + Record& record = *mRecords.at(index); if (record.isDeleted()) { throw std::runtime_error("attempt to touch deleted record"); @@ -302,7 +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().mPlugin = 0; } template @@ -317,7 +311,7 @@ namespace CSMWorld int index = touchRecordImp(id); if (index >= 0) { - mRecords.at(index).get().mPlugin = 0; + mRecords.at(index)->get().mPlugin = 0; return true; } @@ -344,15 +338,15 @@ namespace CSMWorld if (iter==mIndex.end()) { - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + std::unique_ptr > record2(new Record); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = record; - insertRecord (record2, getAppendIndex (id)); + insertRecord (std::move(record2), getAppendIndex (id)); } else { - mRecords[iter->second].setModified (record); + mRecords[iter->second]->setModified (record); } } @@ -365,7 +359,7 @@ namespace CSMWorld template std::string Collection::getId (int index) const { - return IdAccessorT().getId (mRecords.at (index).get()); + return IdAccessorT().getId (mRecords.at (index)->get()); } template @@ -388,13 +382,13 @@ namespace CSMWorld template QVariant Collection::getData (int index, int column) const { - return mColumns.at (column)->get (mRecords.at (index)); + return mColumns.at (column)->get (*mRecords.at (index)); } template void Collection::setData (int index, int column, const QVariant& data) { - return mColumns.at (column)->set (mRecords.at (index), data); + return mColumns.at (column)->set (*mRecords.at (index), data); } template @@ -421,8 +415,8 @@ namespace CSMWorld template void Collection::merge() { - for (typename std::vector >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) - iter->merge(); + for (typename std::vector > >::iterator iter (mRecords.begin()); iter!=mRecords.end(); ++iter) + (*iter)->merge(); purge(); } @@ -434,7 +428,7 @@ namespace CSMWorld while (i (mRecords.size())) { - if (mRecords[i].isErased()) + if (mRecords[i]->isErased()) removeRows (i, 1); else ++i; @@ -475,15 +469,15 @@ namespace CSMWorld IdAccessorT().setId(record, id); record.blank(); - Record record2; - record2.mState = Record::State_ModifiedOnly; - record2.mModified = record; + std::unique_ptr > record2(new Record); + record2->mState = Record::State_ModifiedOnly; + record2->mModified = record; - insertRecord (record2, getAppendIndex (id, type), type); + insertRecord (std::move(record2), getAppendIndex (id, type), type); } template - int Collection::searchId (const std::string& id) const + int Collection::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase(id); @@ -496,18 +490,19 @@ namespace CSMWorld } template - void Collection::replace (int index, const RecordBase& record) + void Collection::replace (int index, std::unique_ptr record) { - mRecords.at (index) = dynamic_cast&> (record); + std::unique_ptr > tmp(static_cast*>(record.release())); + mRecords.at (index) = std::move(tmp); } template - void Collection::appendRecord (const RecordBase& record, + void Collection::appendRecord (std::unique_ptr record, UniversalId::Type type) { - insertRecord (record, - getAppendIndex (IdAccessorT().getId ( - dynamic_cast&> (record).get()), type), type); + int index = + getAppendIndex(IdAccessorT().getId(static_cast*>(record.get())->get()), type); + insertRecord (std::move(record), index, type); } template @@ -525,8 +520,8 @@ namespace CSMWorld for (typename std::map::const_iterator iter = mIndex.begin(); iter!=mIndex.end(); ++iter) { - if (listDeleted || !mRecords[iter->second].isDeleted()) - ids.push_back (IdAccessorT().getId (mRecords[iter->second].get())); + if (listDeleted || !mRecords[iter->second]->isDeleted()) + ids.push_back (IdAccessorT().getId (mRecords[iter->second]->get())); } return ids; @@ -536,46 +531,52 @@ namespace CSMWorld const Record& Collection::getRecord (const std::string& id) const { int index = getIndex (id); - return mRecords.at (index); + return *mRecords.at (index); } template const Record& Collection::getRecord (int index) const { - return mRecords.at (index); + return *mRecords.at (index); } template - void Collection::insertRecord (const RecordBase& record, int index, + void Collection::insertRecord (std::unique_ptr record, int index, UniversalId::Type type) { - if (index<0 || index>static_cast (mRecords.size())) + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) throw std::runtime_error ("index out of range"); - const Record& record2 = dynamic_cast&> (record); + std::unique_ptr > record2(static_cast*>(record.release())); + std::string lowerId = Misc::StringUtils::lowerCase(IdAccessorT().getId(record2->get())); - mRecords.insert (mRecords.begin()+index, record2); + if (index == size) + mRecords.push_back (std::move(record2)); + else + mRecords.insert (mRecords.begin()+index, std::move(record2)); - if (index (mRecords.size())-1) + if (index < size-1) { - for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); - ++iter) - if (iter->second>=index) - ++(iter->second); + for (std::map::iterator iter (mIndex.begin()); iter!=mIndex.end(); ++iter) + { + if (iter->second >= index) + ++(iter->second); + } } - mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (IdAccessorT().getId ( - record2.get())), index)); + mIndex.insert (std::make_pair (lowerId, index)); } template - void Collection::setRecord (int index, const Record& record) + void Collection::setRecord (int index, + std::unique_ptr > record) { - if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index).get()))!= - Misc::StringUtils::lowerCase (IdAccessorT().getId (record.get()))) + if (Misc::StringUtils::lowerCase (IdAccessorT().getId (mRecords.at (index)->get())) != + Misc::StringUtils::lowerCase (IdAccessorT().getId (record->get()))) throw std::runtime_error ("attempt to change the ID of a record"); - mRecords.at (index) = record; + mRecords.at (index) = std::move(record); } template diff --git a/apps/opencs/model/world/collectionbase.cpp b/apps/opencs/model/world/collectionbase.cpp index 6134dc1727..f20fc643e2 100644 --- a/apps/opencs/model/world/collectionbase.cpp +++ b/apps/opencs/model/world/collectionbase.cpp @@ -8,6 +8,11 @@ CSMWorld::CollectionBase::CollectionBase() {} CSMWorld::CollectionBase::~CollectionBase() {} +int CSMWorld::CollectionBase::getInsertIndex (const std::string& id, UniversalId::Type type, RecordBase *record) const +{ + return getAppendIndex(id, type); +} + int CSMWorld::CollectionBase::searchColumnIndex (Columns::ColumnId id) const { int columns = getColumns(); diff --git a/apps/opencs/model/world/collectionbase.hpp b/apps/opencs/model/world/collectionbase.hpp index bac790c5d0..be6131ee52 100644 --- a/apps/opencs/model/world/collectionbase.hpp +++ b/apps/opencs/model/world/collectionbase.hpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include "universalid.hpp" #include "columns.hpp" @@ -60,17 +62,17 @@ namespace CSMWorld UniversalId::Type type = UniversalId::Type_None) = 0; ///< \param type Will be ignored, unless the collection supports multiple record types - virtual int searchId (const std::string& id) const = 0; + virtual int searchId(std::string_view id) const = 0; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - virtual void replace (int index, const RecordBase& record) = 0; + virtual void replace (int index, std::unique_ptr record) = 0; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. ///< \param type Will be ignored, unless the collection supports multiple record types - virtual void appendRecord (const RecordBase& record, + virtual void appendRecord (std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) = 0; ///< If the record type does not match, an exception is thrown. @@ -99,6 +101,12 @@ namespace CSMWorld /// /// \return Success? + virtual int getInsertIndex (const std::string& id, + UniversalId::Type type = UniversalId::Type_None, + RecordBase *record = nullptr) const; + ///< Works like getAppendIndex unless an overloaded method uses the record pointer + /// to get additional info about the record that results in an alternative index. + int searchColumnIndex (Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, -1 is returned. diff --git a/apps/opencs/model/world/columnbase.cpp b/apps/opencs/model/world/columnbase.cpp index cf333c1b1a..6f7bb12b3c 100644 --- a/apps/opencs/model/world/columnbase.cpp +++ b/apps/opencs/model/world/columnbase.cpp @@ -104,7 +104,8 @@ bool CSMWorld::ColumnBase::isId (Display display) bool CSMWorld::ColumnBase::isText (Display display) { return display==Display_String || display==Display_LongString || - display==Display_String32 || display==Display_LongString256; + display==Display_String32 || display==Display_String64 || + display==Display_LongString256; } bool CSMWorld::ColumnBase::isScript (Display display) diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index 6dc58bd634..b7c0d8a248 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -135,6 +135,7 @@ namespace CSMWorld Display_InfoCondVar, Display_InfoCondComp, Display_String32, + Display_String64, Display_LongString256, Display_BookType, Display_BloodType, diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 17518937c2..e0148d6cc6 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -334,7 +334,8 @@ namespace CSMWorld template struct NameColumn : public Column { - NameColumn() : Column (Columns::ColumnId_Name, ColumnBase::Display_String) {} + NameColumn(ColumnBase::Display display = ColumnBase::Display_String) + : Column (Columns::ColumnId_Name, display) {} QVariant get (const Record& record) const override { diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index cf04d96753..6eefdb6e21 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -371,6 +371,7 @@ namespace CSMWorld { ColumnId_Skill7, "Skill 7" }, { ColumnId_Persistent, "Persistent" }, + { ColumnId_Blocked, "Blocked" }, { -1, 0 } // end marker }; @@ -391,7 +392,7 @@ int CSMWorld::Columns::getId (const std::string& name) std::string name2 = Misc::StringUtils::lowerCase (name); for (int i=0; sNames[i].mName; ++i) - if (Misc::StringUtils::ciEqual(sNames[i].mName, name2)) + if (Misc::StringUtils::ciEqual(std::string_view(sNames[i].mName), name2)) return sNames[i].mId; return -1; diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 16482d4898..8cf02b46ac 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -344,6 +344,7 @@ namespace CSMWorld ColumnId_FactionAttrib2 = 312, ColumnId_Persistent = 313, + ColumnId_Blocked = 314, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. diff --git a/apps/opencs/model/world/commanddispatcher.cpp b/apps/opencs/model/world/commanddispatcher.cpp index 36b3ba2e00..c5d78b6586 100644 --- a/apps/opencs/model/world/commanddispatcher.cpp +++ b/apps/opencs/model/world/commanddispatcher.cpp @@ -141,7 +141,6 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons std::unique_ptr modifyCell; - std::unique_ptr modifyDataRefNum; int columnId = model->data (index, ColumnBase::Role_ColumnId).toInt(); @@ -170,14 +169,8 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons if (cellId.find ('#')!=std::string::npos) { - // Need to recalculate the cell and (if necessary) clear the instance's refNum + // Need to recalculate the cell modifyCell.reset (new UpdateCellCommand (model2, row)); - - // Not sure which model this should be applied to - int refNumColumn = model2.searchColumnIndex (Columns::ColumnId_RefNum); - - if (refNumColumn!=-1) - modifyDataRefNum.reset (new ModifyCommand(*model, model->index(row, refNumColumn), 0)); } } } @@ -191,8 +184,6 @@ void CSMWorld::CommandDispatcher::executeModify (QAbstractItemModel *model, cons CommandMacro macro (mDocument.getUndoStack()); macro.push (modifyData.release()); macro.push (modifyCell.release()); - if (modifyDataRefNum.get()) - macro.push (modifyDataRefNum.release()); } else mDocument.getUndoStack().push (modifyData.release()); diff --git a/apps/opencs/model/world/commands.cpp b/apps/opencs/model/world/commands.cpp index 5ec7401dc9..a264ebef7f 100644 --- a/apps/opencs/model/world/commands.cpp +++ b/apps/opencs/model/world/commands.cpp @@ -24,11 +24,11 @@ CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUnd , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mTable.getRecord(mId).clone()); } void CSMWorld::TouchCommand::redo() { + mOld.reset(mTable.getRecord(mId).clone().get()); mChanged = mTable.touchRecord(mId); } @@ -36,7 +36,7 @@ void CSMWorld::TouchCommand::undo() { if (mChanged) { - mTable.setRecord(mId, *mOld); + mTable.setRecord(mId, std::move(mOld)); mChanged = false; } } @@ -159,7 +159,6 @@ CSMWorld::TouchLandCommand::TouchLandCommand(IdTable& landTable, IdTable& ltexTa , mChanged(false) { setText(("Touch " + mId).c_str()); - mOld.reset(mLands.getRecord(mId).clone()); } const std::string& CSMWorld::TouchLandCommand::getOriginId() const @@ -175,13 +174,14 @@ const std::string& CSMWorld::TouchLandCommand::getDestinationId() const void CSMWorld::TouchLandCommand::onRedo() { mChanged = mLands.touchRecord(mId); + if (mChanged) mOld.reset(mLands.getRecord(mId).clone().get()); } void CSMWorld::TouchLandCommand::onUndo() { if (mChanged) { - mLands.setRecord(mId, *mOld); + mLands.setRecord(mId, std::move(mOld)); mChanged = false; } } @@ -190,13 +190,16 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI const QVariant& new_, QUndoCommand* parent) : QUndoCommand (parent), mModel (&model), mIndex (index), mNew (new_), mHasRecordState(false), mOldRecordState(CSMWorld::RecordBase::State_BaseOnly) { - if (QAbstractProxyModel *proxy = dynamic_cast (&model)) + if (QAbstractProxyModel *proxy = dynamic_cast (mModel)) { // Replace proxy with actual model - mIndex = proxy->mapToSource (index); + mIndex = proxy->mapToSource (mIndex); mModel = proxy->sourceModel(); } +} +void CSMWorld::ModifyCommand::redo() +{ if (mIndex.parent().isValid()) { CSMWorld::IdTree* tree = &dynamic_cast(*mModel); @@ -223,10 +226,7 @@ CSMWorld::ModifyCommand::ModifyCommand (QAbstractItemModel& model, const QModelI mRecordStateIndex = table->index(rowIndex, stateColumnIndex); mOldRecordState = static_cast(table->data(mRecordStateIndex).toInt()); } -} -void CSMWorld::ModifyCommand::redo() -{ mOld = mModel->data (mIndex, Qt::EditRole); mModel->setData (mIndex, mNew); } @@ -291,20 +291,19 @@ void CSMWorld::CreateCommand::undo() } CSMWorld::RevertCommand::RevertCommand (IdTable& model, const std::string& id, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr) +: QUndoCommand (parent), mModel (model), mId (id), mOld(nullptr) { setText (("Revert record " + id).c_str()); - - mOld = model.getRecord (id).clone(); } CSMWorld::RevertCommand::~RevertCommand() { - delete mOld; } void CSMWorld::RevertCommand::redo() { + mOld = mModel.getRecord (mId).clone(); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); @@ -322,25 +321,24 @@ void CSMWorld::RevertCommand::redo() void CSMWorld::RevertCommand::undo() { - mModel.setRecord (mId, *mOld); + mModel.setRecord (mId, std::move(mOld)); } CSMWorld::DeleteCommand::DeleteCommand (IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) -: QUndoCommand (parent), mModel (model), mId (id), mOld (nullptr), mType(type) +: QUndoCommand (parent), mModel (model), mId (id), mOld(nullptr), mType(type) { setText (("Delete record " + id).c_str()); - - mOld = model.getRecord (id).clone(); } CSMWorld::DeleteCommand::~DeleteCommand() { - delete mOld; } void CSMWorld::DeleteCommand::redo() { + mOld = mModel.getRecord (mId).clone(); + int column = mModel.findColumnIndex (Columns::ColumnId_Modification); QModelIndex index = mModel.getModelIndex (mId, column); @@ -358,7 +356,7 @@ void CSMWorld::DeleteCommand::redo() void CSMWorld::DeleteCommand::undo() { - mModel.setRecord (mId, *mOld, mType); + mModel.setRecord (mId, std::move(mOld), mType); } @@ -424,18 +422,19 @@ void CSMWorld::CreatePathgridCommand::redo() { CreateCommand::redo(); - Record record = static_cast& >(mModel.getRecord(mId)); - record.get().blank(); - record.get().mCell = mId; + std::unique_ptr > record + = std::make_unique >(static_cast& >(mModel.getRecord(mId))); + record->get().blank(); + record->get().mCell = mId; std::pair coords = CellCoordinates::fromId(mId); if (coords.second) { - record.get().mData.mX = coords.first.getX(); - record.get().mData.mY = coords.first.getY(); + record->get().mData.mX = coords.first.getX(); + record->get().mData.mY = coords.first.getY(); } - mModel.setRecord(mId, record, mType); + mModel.setRecord(mId, std::move(record), mType); } CSMWorld::UpdateCellCommand::UpdateCellCommand (IdTable& model, int row, QUndoCommand *parent) @@ -499,8 +498,8 @@ CSMWorld::DeleteNestedCommand::DeleteNestedCommand (IdTree& model, void CSMWorld::DeleteNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.removeRows (mNestedRow, 1, parentIndex); mModifyParentCommand->redo(); + mModel.removeRows (mNestedRow, 1, parentIndex); } @@ -530,8 +529,8 @@ CSMWorld::AddNestedCommand::AddNestedCommand(IdTree& model, const std::string& i void CSMWorld::AddNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); - mModel.addNestedRow (parentIndex, mNewRow); mModifyParentCommand->redo(); + mModel.addNestedRow (parentIndex, mNewRow); } void CSMWorld::AddNestedCommand::undo() diff --git a/apps/opencs/model/world/commands.hpp b/apps/opencs/model/world/commands.hpp index 33608304f4..2ed629ef5c 100644 --- a/apps/opencs/model/world/commands.hpp +++ b/apps/opencs/model/world/commands.hpp @@ -203,7 +203,7 @@ namespace CSMWorld { IdTable& mModel; std::string mId; - RecordBase *mOld; + std::unique_ptr mOld; // not implemented RevertCommand (const RevertCommand&); @@ -224,7 +224,7 @@ namespace CSMWorld { IdTable& mModel; std::string mId; - RecordBase *mOld; + std::unique_ptr mOld; UniversalId::Type mType; // not implemented diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 4ccd2a06db..3723dac46f 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -84,6 +84,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat defines["preLightEnv"] = "0"; // Apply environment maps after lighting like Morrowind defines["radialFog"] = "0"; defines["lightingModel"] = "0"; + defines["reverseZ"] = "0"; for (const auto& define : shadowDefines) defines[define.first] = define.second; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); @@ -130,7 +131,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat mFactions.addColumn (new StringIdColumn); mFactions.addColumn (new RecordStateColumn); mFactions.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Faction)); - mFactions.addColumn (new NameColumn); + mFactions.addColumn (new NameColumn(ColumnBase::Display_String32)); mFactions.addColumn (new AttributesColumn (0)); mFactions.addColumn (new AttributesColumn (1)); mFactions.addColumn (new HiddenColumn); @@ -339,7 +340,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, bool fsStrict, const Files::Pat mCells.addColumn (new StringIdColumn); mCells.addColumn (new RecordStateColumn); mCells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Cell)); - mCells.addColumn (new NameColumn); + mCells.addColumn (new NameColumn(ColumnBase::Display_String64)); mCells.addColumn (new FlagColumn (Columns::ColumnId_SleepForbidden, ESM::Cell::NoSleep)); mCells.addColumn (new FlagColumn (Columns::ColumnId_InteriorWater, ESM::Cell::HasWater, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); @@ -917,8 +918,8 @@ const CSMWorld::MetaData& CSMWorld::Data::getMetaData() const void CSMWorld::Data::setMetaData (const MetaData& metaData) { - Record record (RecordBase::State_ModifiedOnly, nullptr, &metaData); - mMetaData.setRecord (0, record); + mMetaData.setRecord (0, std::make_unique >( + Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) @@ -958,6 +959,25 @@ void CSMWorld::Data::merge() mGlobals.merge(); } +int CSMWorld::Data::getTotalRecords (const std::vector& files) +{ + int records = 0; + + std::unique_ptr reader = std::unique_ptr(new ESM::ESMReader); + + for (unsigned int i = 0; i < files.size(); ++i) + { + if (!boost::filesystem::exists(files[i])) + continue; + + reader->open(files[i].string()); + records += reader->getRecordCount(); + reader->close(); + } + + return records; +} + int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base, bool project) { // Don't delete the Reader yet. Some record types store a reference to the Reader to handle on-demand loading @@ -983,7 +1003,8 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base metaData.mId = "sys::meta"; metaData.load (*mReader); - mMetaData.setRecord (0, Record (RecordBase::State_ModifiedOnly, nullptr, &metaData)); + mMetaData.setRecord (0, std::make_unique >( + Record (RecordBase::State_ModifiedOnly, nullptr, &metaData))); } return mReader->getRecordCount(); @@ -1011,10 +1032,10 @@ void CSMWorld::Data::loadFallbackEntries() ESM::Static newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Static); + std::unique_ptr > record(new CSMWorld::Record); + record->mBase = newMarker; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord (std::move(record), CSMWorld::UniversalId::Type_Static); } } @@ -1025,10 +1046,10 @@ void CSMWorld::Data::loadFallbackEntries() ESM::Door newMarker; newMarker.mId = marker.first; newMarker.mModel = marker.second; - CSMWorld::Record record; - record.mBase = newMarker; - record.mState = CSMWorld::RecordBase::State_BaseOnly; - mReferenceables.appendRecord (record, CSMWorld::UniversalId::Type_Door); + std::unique_ptr > record(new CSMWorld::Record); + record->mBase = newMarker; + record->mState = CSMWorld::RecordBase::State_BaseOnly; + mReferenceables.appendRecord (std::move(record), CSMWorld::UniversalId::Type_Door); } } } diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 5e2054b712..b1c20b8629 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -118,7 +118,7 @@ namespace CSMWorld const ESM::Dialogue *mDialogue; // last loaded dialogue bool mBase; bool mProject; - std::map > mRefLoadCache; + std::map > mRefLoadCache; int mReaderIndex; bool mFsStrict; @@ -292,6 +292,8 @@ namespace CSMWorld void merge(); ///< Merge modified into base. + int getTotalRecords (const std::vector& files); // for better loading bar + int startLoading (const boost::filesystem::path& path, bool base, bool project); ///< Begin merging content of a file into base or modified. /// diff --git a/apps/opencs/model/world/idcollection.cpp b/apps/opencs/model/world/idcollection.cpp new file mode 100644 index 0000000000..d06a47e32b --- /dev/null +++ b/apps/opencs/model/world/idcollection.cpp @@ -0,0 +1,43 @@ +#include "idcollection.hpp" + +namespace CSMWorld +{ + template<> + int IdCollection >::load (ESM::ESMReader& reader, bool base) + { + Pathgrid record; + bool isDeleted = false; + + loadRecord (record, reader, isDeleted); + + std::string id = IdAccessor().getId (record); + int index = this->searchId (id); + + if (record.mPoints.empty() || record.mEdges.empty()) + isDeleted = true; + + if (isDeleted) + { + if (index==-1) + { + // deleting a record that does not exist + // ignore it for now + /// \todo report the problem to the user + return -1; + } + + if (base) + { + this->removeRows (index, 1); + return -1; + } + + std::unique_ptr > baseRecord(new Record(this->getRecord(index))); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); + return index; + } + + return load (record, base, index); + } +} diff --git a/apps/opencs/model/world/idcollection.hpp b/apps/opencs/model/world/idcollection.hpp index 7849aab926..bbc49f18c1 100644 --- a/apps/opencs/model/world/idcollection.hpp +++ b/apps/opencs/model/world/idcollection.hpp @@ -5,6 +5,7 @@ #include "collection.hpp" #include "land.hpp" +#include "pathgrid.hpp" namespace CSMWorld { @@ -83,9 +84,9 @@ namespace CSMWorld return -1; } - Record baseRecord = this->getRecord (index); - baseRecord.mState = RecordBase::State_Deleted; - this->setRecord (index, baseRecord); + std::unique_ptr > baseRecord(new Record(this->getRecord(index))); + baseRecord->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(baseRecord)); return index; } @@ -96,30 +97,31 @@ namespace CSMWorld int IdCollection::load (const ESXRecordT& record, bool base, int index) { - if (index==-2) + if (index==-2) // index unknown index = this->searchId (IdAccessorT().getId (record)); if (index==-1) { // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; + std::unique_ptr > record2(new Record); + record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record2->mBase : record2->mModified) = record; index = this->getSize(); - this->appendRecord (record2); + this->appendRecord(std::move(record2)); } else { // old record - Record record2 = Collection::getRecord (index); + std::unique_ptr > record2( + new Record(Collection::getRecord(index))); if (base) - record2.mBase = record; + record2->mBase = record; else - record2.setModified (record); + record2->setModified(record); - this->setRecord (index, record2); + this->setRecord(index, std::move(record2)); } return index; @@ -133,7 +135,7 @@ namespace CSMWorld if (index==-1) return false; - Record record = Collection::getRecord (index); + const Record& record = Collection::getRecord (index); if (record.isDeleted()) return false; @@ -144,12 +146,17 @@ namespace CSMWorld } else { - record.mState = RecordBase::State_Deleted; - this->setRecord (index, record); + std::unique_ptr > record2( + new Record(Collection::getRecord(index))); + record2->mState = RecordBase::State_Deleted; + this->setRecord(index, std::move(record2)); } return true; } + + template<> + int IdCollection >::load(ESM::ESMReader& reader, bool base); } #endif diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 30fe6f639a..5b4a9b31bc 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -123,6 +123,14 @@ Qt::ItemFlags CSMWorld::IdTable::flags (const QModelIndex & index) const if (mIdCollection->getColumn (index.column()).isUserEditable()) flags |= Qt::ItemIsEditable; + int blockedColumn = searchColumnIndex(Columns::ColumnId_Blocked); + if (blockedColumn != -1 && blockedColumn != index.column()) + { + bool isBlocked = mIdCollection->getData(index.row(), blockedColumn).toInt(); + if (isBlocked) + flags = Qt::ItemIsSelectable; // not enabled (to grey out) + } + return flags; } @@ -191,7 +199,7 @@ void CSMWorld::IdTable::cloneRecord(const std::string& origin, const std::string& destination, CSMWorld::UniversalId::Type type) { - int index = mIdCollection->getAppendIndex (destination); + int index = mIdCollection->getAppendIndex (destination, type); beginInsertRows (QModelIndex(), index, index); mIdCollection->cloneRecord(origin, destination, type); @@ -228,23 +236,30 @@ QModelIndex CSMWorld::IdTable::getModelIndex (const std::string& id, int column) return QModelIndex(); } -void CSMWorld::IdTable::setRecord (const std::string& id, const RecordBase& record, CSMWorld::UniversalId::Type type) +void CSMWorld::IdTable::setRecord (const std::string& id, + std::unique_ptr record, CSMWorld::UniversalId::Type type) { int index = mIdCollection->searchId (id); if (index==-1) { - index = mIdCollection->getAppendIndex (id, type); + // For info records, appendRecord may use a different index than the one returned by + // getAppendIndex (because of prev/next links). This can result in the display not + // updating correctly after an undo + // + // Use an alternative method to get the correct index. For non-Info records the + // record pointer is ignored and internally calls getAppendIndex. + int index2 = mIdCollection->getInsertIndex (id, type, record.get()); - beginInsertRows (QModelIndex(), index, index); + beginInsertRows (QModelIndex(), index2, index2); - mIdCollection->appendRecord (record, type); + mIdCollection->appendRecord (std::move(record), type); endInsertRows(); } else { - mIdCollection->replace (index, record); + mIdCollection->replace (index, std::move(record)); emit dataChanged (CSMWorld::IdTable::index (index, 0), CSMWorld::IdTable::index (index, mIdCollection->getColumns()-1)); } diff --git a/apps/opencs/model/world/idtable.hpp b/apps/opencs/model/world/idtable.hpp index 6b7b8d3182..c38e1b5f9c 100644 --- a/apps/opencs/model/world/idtable.hpp +++ b/apps/opencs/model/world/idtable.hpp @@ -2,6 +2,7 @@ #define CSM_WOLRD_IDTABLE_H #include +#include #include "idtablebase.hpp" #include "universalid.hpp" @@ -67,7 +68,7 @@ namespace CSMWorld QModelIndex getModelIndex (const std::string& id, int column) const override; - void setRecord (const std::string& id, const RecordBase& record, + void setRecord (const std::string& id, std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None); ///< Add record or overwrite existing record. diff --git a/apps/opencs/model/world/idtableproxymodel.cpp b/apps/opencs/model/world/idtableproxymodel.cpp index 3e24f8d12e..9d9dd7db3d 100644 --- a/apps/opencs/model/world/idtableproxymodel.cpp +++ b/apps/opencs/model/world/idtableproxymodel.cpp @@ -121,8 +121,11 @@ QString CSMWorld::IdTableProxyModel::getRecordId(int sourceRow) const void CSMWorld::IdTableProxyModel::refreshFilter() { - updateColumnMap(); - invalidateFilter(); + if (mFilter) + { + updateColumnMap(); + invalidateFilter(); + } } void CSMWorld::IdTableProxyModel::sourceRowsInserted(const QModelIndex &parent, int /*start*/, int end) diff --git a/apps/opencs/model/world/infocollection.cpp b/apps/opencs/model/world/infocollection.cpp index be73aece6f..d58a8327f2 100644 --- a/apps/opencs/model/world/infocollection.cpp +++ b/apps/opencs/model/world/infocollection.cpp @@ -2,12 +2,74 @@ #include #include +#include #include #include #include +namespace CSMWorld +{ + template<> + void Collection >::removeRows (int index, int count) + { + mRecords.erase(mRecords.begin()+index, mRecords.begin()+index+count); + + // index map is updated in InfoCollection::removeRows() + } + + template<> + void Collection >::insertRecord (std::unique_ptr record, + int index, UniversalId::Type type) + { + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); + + std::unique_ptr > record2(static_cast*>(record.release())); + + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin()+index, std::move(record2)); + + // index map is updated in InfoCollection::insertRecord() + } + + template<> + bool Collection >::reorderRowsImp (int baseIndex, + const std::vector& newOrder) + { + if (!newOrder.empty()) + { + int size = static_cast(newOrder.size()); + + // check that all indices are present + std::vector test(newOrder); + std::sort(test.begin(), test.end()); + if (*test.begin() != 0 || *--test.end() != size-1) + return false; + + // reorder records + std::vector > > buffer(size); + + // FIXME: BUG: undo does not remove modified flag + for (int i = 0; i < size; ++i) + { + buffer[newOrder[i]] = std::move(mRecords[baseIndex+i]); + buffer[newOrder[i]]->setModified(buffer[newOrder[i]]->get()); + } + + std::move(buffer.begin(), buffer.end(), mRecords.begin()+baseIndex); + + // index map is updated in InfoCollection::reorderRows() + } + + return true; + } +} + void CSMWorld::InfoCollection::load (const Info& record, bool base) { int index = searchId (record.mId); @@ -15,74 +77,96 @@ void CSMWorld::InfoCollection::load (const Info& record, bool base) if (index==-1) { // new record - Record record2; - record2.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2.mBase : record2.mModified) = record; + std::unique_ptr > record2(new Record); + record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record2->mBase : record2->mModified) = record; - std::string topic = Misc::StringUtils::lowerCase (record2.get().mTopicId); - - if (!record2.get().mPrev.empty()) - { - index = getInfoIndex (record2.get().mPrev, topic); - - if (index!=-1) - ++index; - } - - if (index==-1 && !record2.get().mNext.empty()) - { - index = getInfoIndex (record2.get().mNext, topic); - } - - if (index==-1) - { - Range range = getTopicRange (topic); - - index = std::distance (getRecords().begin(), range.second); - } - - insertRecord (record2, index); + appendRecord(std::move(record2)); } else { // old record - Record record2 = getRecord (index); + std::unique_ptr > record2(new Record(getRecord(index))); if (base) - record2.mBase = record; + record2->mBase = record; else - record2.setModified (record); + record2->setModified (record); - setRecord (index, record2); + setRecord (index, std::move(record2)); } } -int CSMWorld::InfoCollection::getInfoIndex (const std::string& id, const std::string& topic) const +int CSMWorld::InfoCollection::getInfoIndex(std::string_view id, std::string_view topic) const { - std::string fullId = Misc::StringUtils::lowerCase (topic) + "#" + id; + // find the topic first + std::unordered_map > >::const_iterator iter + = mInfoIndex.find(Misc::StringUtils::lowerCase(topic)); - std::pair range = getTopicRange (topic); + if (iter == mInfoIndex.end()) + return -1; - for (; range.first!=range.second; ++range.first) - if (Misc::StringUtils::ciEqual(range.first->get().mId, fullId)) - return std::distance (getRecords().begin(), range.first); + // brute force loop + for (std::vector >::const_iterator it = iter->second.begin(); + it != iter->second.end(); ++it) + { + if (Misc::StringUtils::ciEqual(it->first, id)) + return it->second; + } return -1; } -int CSMWorld::InfoCollection::getAppendIndex (const std::string& id, UniversalId::Type type) const +// Calling insertRecord() using index from getInsertIndex() needs to take into account of +// prev/next records; an example is deleting a record then undo +int CSMWorld::InfoCollection::getInsertIndex (const std::string& id, + UniversalId::Type type, RecordBase *record) const { - std::string::size_type separator = id.find_last_of ('#'); + if (record == nullptr) + { + std::string::size_type separator = id.find_last_of('#'); - if (separator==std::string::npos) - throw std::runtime_error ("invalid info ID: " + id); + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + id); - std::pair range = getTopicRange (id.substr (0, separator)); + std::pair range = getTopicRange(id.substr(0, separator)); - if (range.first==range.second) - return Collection >::getAppendIndex (id, type); + if (range.first == range.second) + return Collection >::getAppendIndex(id, type); - return std::distance (getRecords().begin(), range.second); + return std::distance(getRecords().begin(), range.second); + } + + int index = -1; + + const Info& info = static_cast*>(record)->get(); + std::string topic = info.mTopicId; + + // if the record has a prev, find its index value + if (!info.mPrev.empty()) + { + index = getInfoIndex(info.mPrev, topic); + + if (index != -1) + ++index; // if prev exists, set current index to one above prev + } + + // if prev doesn't exist or not found and the record has a next, find its index value + if (index == -1 && !info.mNext.empty()) + { + // if next exists, use its index as the current index + index = getInfoIndex(info.mNext, topic); + } + + // if next doesn't exist or not found (i.e. neither exist yet) then start a new one + if (index == -1) + { + Range range = getTopicRange(topic); // getTopicRange converts topic to lower case first + + index = std::distance(getRecords().begin(), range.second); + } + + return index; } bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector& newOrder) @@ -99,7 +183,17 @@ bool CSMWorld::InfoCollection::reorderRows (int baseIndex, const std::vector >::reorderRowsImp(baseIndex, newOrder)) + return false; + + // adjust index + int size = static_cast(newOrder.size()); + for (auto& [hash, infos] : mInfoIndex) + for (auto& [a, b] : infos) + if (b >= baseIndex && b < baseIndex + size) + b = newOrder.at(b - baseIndex) + baseIndex; + + return true; } void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue) @@ -126,9 +220,9 @@ void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ES } else { - Record record = getRecord (index); - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + std::unique_ptr > record(new Record(getRecord(index))); + record->mState = RecordBase::State_Deleted; + setRecord (index, std::move(record)); } } else @@ -142,73 +236,54 @@ void CSMWorld::InfoCollection::load (ESM::ESMReader& reader, bool base, const ES CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange (const std::string& topic) const { - std::string topic2 = Misc::StringUtils::lowerCase (topic); + std::string lowerTopic = Misc::StringUtils::lowerCase (topic); - std::map::const_iterator iter = getIdMap().lower_bound (topic2); + // find the topic + std::unordered_map > >::const_iterator iter + = mInfoIndex.find(lowerTopic); - // Skip invalid records: The beginning of a topic string could be identical to another topic - // string. - for (; iter!=getIdMap().end(); ++iter) - { - std::string testTopicId = - Misc::StringUtils::lowerCase (getRecord (iter->second).get().mTopicId); - - if (testTopicId==topic2) - break; - - std::size_t size = topic2.size(); - - if (testTopicId.size()second; - - while (begin != getRecords().begin()) + // topic found, find the starting index + int low = INT_MAX; + for (std::vector >::const_iterator it = iter->second.begin(); + it != iter->second.end(); ++it) { - if (!Misc::StringUtils::ciEqual(begin->get().mTopicId, topic2)) - { - // we've gone one too far, go back - ++begin; - break; - } - --begin; + low = std::min(low, it->second); } - // Find end - RecordConstIterator end = begin; + RecordConstIterator begin = getRecords().begin() + low; - for (; end!=getRecords().end(); ++end) - if (!Misc::StringUtils::ciEqual(end->get().mTopicId, topic2)) - break; + // Find end (one past the range) + RecordConstIterator end = begin + iter->second.size(); + + assert(static_cast(std::distance(begin, end)) == iter->second.size()); return Range (begin, end); } void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId) { - std::string id = Misc::StringUtils::lowerCase(dialogueId); std::vector erasedRecords; - std::map::const_iterator current = getIdMap().lower_bound(id); - std::map::const_iterator end = getIdMap().end(); - for (; current != end; ++current) + Range range = getTopicRange(dialogueId); // getTopicRange converts dialogueId to lower case first + + for (; range.first != range.second; ++range.first) { - Record record = getRecord(current->second); + const Record& record = **range.first; if (Misc::StringUtils::ciEqual(dialogueId, record.get().mTopicId)) { if (record.mState == RecordBase::State_ModifiedOnly) { - erasedRecords.push_back(current->second); + erasedRecords.push_back(range.first - getRecords().begin()); } else { - record.mState = RecordBase::State_Deleted; - setRecord(current->second, record); + std::unique_ptr > record2(new Record(record)); + record2->mState = RecordBase::State_Deleted; + setRecord(range.first - getRecords().begin(), std::move(record2)); } } else @@ -223,3 +298,105 @@ void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId erasedRecords.pop_back(); } } + +// FIXME: removing a record should adjust prev/next and mark those records as modified +// accordingly (also consider undo) +void CSMWorld::InfoCollection::removeRows (int index, int count) +{ + Collection >::removeRows(index, count); // erase records only + + for (std::unordered_map > >::iterator iter + = mInfoIndex.begin(); iter != mInfoIndex.end();) + { + for (std::vector >::iterator it = iter->second.begin(); + it != iter->second.end();) + { + if (it->second >= index) + { + if (it->second >= index+count) + { + it->second -= count; + ++it; + } + else + iter->second.erase(it); + } + else + ++it; + } + + // check for an empty vector + if (iter->second.empty()) + mInfoIndex.erase(iter++); + else + ++iter; + } +} + +void CSMWorld::InfoCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) +{ + std::unique_ptr > record2(new Record); + + record2->mState = Record::State_ModifiedOnly; + record2->mModified.blank(); + + record2->get().mId = id; + + insertRecord(std::move(record2), getInsertIndex(id, type, nullptr), type); // call InfoCollection::insertRecord() +} + +int CSMWorld::InfoCollection::searchId(std::string_view id) const +{ + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + std::string(id)); + + return getInfoIndex(id.substr(separator+1), id.substr(0, separator)); +} + +void CSMWorld::InfoCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) +{ + int index = getInsertIndex(static_cast*>(record.get())->get().mId, type, record.get()); + + insertRecord(std::move(record), index, type); +} + +void CSMWorld::InfoCollection::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) +{ + int size = static_cast(getRecords().size()); + + std::string id = static_cast*>(record.get())->get().mId; + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid info ID: " + id); + + Collection >::insertRecord(std::move(record), index, type); // add records only + + // adjust index + if (index < size-1) + { + for (std::unordered_map > >::iterator iter + = mInfoIndex.begin(); iter != mInfoIndex.end(); ++iter) + { + for (std::vector >::iterator it = iter->second.begin(); + it != iter->second.end(); ++it) + { + if (it->second >= index) + ++(it->second); + } + } + } + + // get iterator for existing topic or a new topic + std::string lowerId = Misc::StringUtils::lowerCase(id); + std::pair > >::iterator, bool> res + = mInfoIndex.insert( + std::make_pair(lowerId.substr(0, separator), + std::vector >())); // empty vector + + // insert info and index + res.first->second.push_back(std::make_pair(lowerId.substr(separator+1), index)); +} diff --git a/apps/opencs/model/world/infocollection.hpp b/apps/opencs/model/world/infocollection.hpp index 8f5aea6012..96061fb03c 100644 --- a/apps/opencs/model/world/infocollection.hpp +++ b/apps/opencs/model/world/infocollection.hpp @@ -1,6 +1,9 @@ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H +#include +#include + #include "collection.hpp" #include "info.hpp" @@ -11,27 +14,52 @@ namespace ESM namespace CSMWorld { + template<> + void Collection >::removeRows (int index, int count); + + template<> + void Collection >::insertRecord (std::unique_ptr record, + int index, UniversalId::Type type); + + template<> + bool Collection >::reorderRowsImp (int baseIndex, + const std::vector& newOrder); + class InfoCollection : public Collection > { public: - typedef std::vector >::const_iterator RecordConstIterator; + typedef std::vector > >::const_iterator RecordConstIterator; typedef std::pair Range; private: + // The general strategy is to keep the records in Collection kept in order (within + // a topic group) while the index lookup maps are not ordered. It is assumed that + // each topic has a small number of infos, which allows the use of vectors for + // iterating through them without too much penalty. + // + // NOTE: topic string as well as id string are stored in lower case. + std::unordered_map > > mInfoIndex; + void load (const Info& record, bool base); - int getInfoIndex (const std::string& id, const std::string& topic) const; + int getInfoIndex(std::string_view id, std::string_view topic) const; ///< Return index for record \a id or -1 (if not present; deleted records are considered) /// /// \param id info ID without topic prefix + // + /// \attention id and topic are assumed to be in lower case public: - int getAppendIndex (const std::string& id, - UniversalId::Type type = UniversalId::Type_None) const override; + int getInsertIndex (const std::string& id, + UniversalId::Type type = UniversalId::Type_None, + RecordBase *record = nullptr) const override; ///< \param type Will be ignored, unless the collection supports multiple record types + /// + /// Works like getAppendIndex unless an overloaded method uses the record pointer + /// to get additional info about the record that results in an alternative index. bool reorderRows (int baseIndex, const std::vector& newOrder) override; ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices @@ -46,6 +74,20 @@ namespace CSMWorld /// the given topic. void removeDialogueInfos(const std::string& dialogueId); + + void removeRows (int index, int count) override; + + void appendBlankRecord (const std::string& id, + UniversalId::Type type = UniversalId::Type_None) override; + + int searchId(std::string_view id) const override; + + void appendRecord (std::unique_ptr record, + UniversalId::Type type = UniversalId::Type_None) override; + + void insertRecord (std::unique_ptr record, + int index, + UniversalId::Type type = UniversalId::Type_None) override; }; } diff --git a/apps/opencs/model/world/nestedidcollection.hpp b/apps/opencs/model/world/nestedidcollection.hpp index 1bd20aeebf..4af864eb42 100644 --- a/apps/opencs/model/world/nestedidcollection.hpp +++ b/apps/opencs/model/world/nestedidcollection.hpp @@ -90,23 +90,23 @@ namespace CSMWorld template void NestedIdCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).addRow(record, position); + getAdapter(Collection::getColumn(column)).addRow(*record, position); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template void NestedIdCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection::getRecord(row)); - getAdapter(Collection::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template @@ -121,13 +121,13 @@ namespace CSMWorld void NestedIdCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setData( - record, data, subRow, subColumn); + *record, data, subRow, subColumn); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template @@ -142,13 +142,13 @@ namespace CSMWorld void NestedIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setTable( - record, nestedTable); + *record, nestedTable); - Collection::setRecord(row, record); + Collection::setRecord(row, std::move(record)); } template diff --git a/apps/opencs/model/world/nestedinfocollection.cpp b/apps/opencs/model/world/nestedinfocollection.cpp index 4abaaf9c02..d404bb9a63 100644 --- a/apps/opencs/model/world/nestedinfocollection.cpp +++ b/apps/opencs/model/world/nestedinfocollection.cpp @@ -35,22 +35,22 @@ namespace CSMWorld void NestedInfoCollection::addNestedRow(int row, int column, int position) { - Record record; - record.assign(Collection >::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection >::getRecord(row)); - getAdapter(Collection >::getColumn(column)).addRow(record, position); + getAdapter(Collection >::getColumn(column)).addRow(*record, position); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } void NestedInfoCollection::removeNestedRows(int row, int column, int subRow) { - Record record; - record.assign(Collection >::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection >::getRecord(row)); - getAdapter(Collection >::getColumn(column)).removeRow(record, subRow); + getAdapter(Collection >::getColumn(column)).removeRow(*record, subRow); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } QVariant NestedInfoCollection::getNestedData (int row, @@ -63,13 +63,13 @@ namespace CSMWorld void NestedInfoCollection::setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) { - Record record; - record.assign(Collection >::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setData( - record, data, subRow, subColumn); + *record, data, subRow, subColumn); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, @@ -82,13 +82,13 @@ namespace CSMWorld void NestedInfoCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { - Record record; - record.assign(Collection >::getRecord(row)); + std::unique_ptr > record(new Record); + record->assign(Collection >::getRecord(row)); getAdapter(Collection >::getColumn(column)).setTable( - record, nestedTable); + *record, nestedTable); - Collection >::setRecord(row, record); + Collection >::setRecord(row, std::move(record)); } int NestedInfoCollection::getNestedRowsCount(int row, int column) const diff --git a/apps/opencs/model/world/pathgrid.hpp b/apps/opencs/model/world/pathgrid.hpp index 22d01b0710..ce74d419e4 100644 --- a/apps/opencs/model/world/pathgrid.hpp +++ b/apps/opencs/model/world/pathgrid.hpp @@ -20,7 +20,7 @@ namespace CSMWorld { std::string mId; - void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection& cells); + void load (ESM::ESMReader &esm, bool &isDeleted, const IdCollection >& cells); void load (ESM::ESMReader &esm, bool &isDeleted); }; } diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 5f67a93b12..bb43612e5e 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -1,6 +1,7 @@ #ifndef CSM_WOLRD_RECORD_H #define CSM_WOLRD_RECORD_H +#include #include namespace CSMWorld @@ -20,9 +21,9 @@ namespace CSMWorld virtual ~RecordBase(); - virtual RecordBase *clone() const = 0; + virtual std::unique_ptr clone() const = 0; - virtual RecordBase *modifiedCopy() const = 0; + virtual std::unique_ptr modifiedCopy() const = 0; virtual void assign (const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. @@ -45,9 +46,9 @@ namespace CSMWorld Record(State state, const ESXRecordT *base = 0, const ESXRecordT *modified = 0); - RecordBase *clone() const override; + std::unique_ptr clone() const override; - RecordBase *modifiedCopy() const override; + std::unique_ptr modifiedCopy() const override; void assign (const RecordBase& record) override; @@ -85,15 +86,16 @@ namespace CSMWorld } template - RecordBase *Record::modifiedCopy() const + std::unique_ptr Record::modifiedCopy() const { - return new Record (State_ModifiedOnly, nullptr, &(this->get())); + return std::make_unique >( + Record(State_ModifiedOnly, nullptr, &(this->get()))); } template - RecordBase *Record::clone() const + std::unique_ptr Record::clone() const { - return new Record (*this); + return std::make_unique >(Record(*this)); } template diff --git a/apps/opencs/model/world/ref.cpp b/apps/opencs/model/world/ref.cpp index b336235909..0b07b484ca 100644 --- a/apps/opencs/model/world/ref.cpp +++ b/apps/opencs/model/world/ref.cpp @@ -2,7 +2,7 @@ #include "cellcoordinates.hpp" -CSMWorld::CellRef::CellRef() : mNew (true) +CSMWorld::CellRef::CellRef() : mNew (true), mIdNum(0) { mRefNum.mIndex = 0; mRefNum.mContentFile = 0; diff --git a/apps/opencs/model/world/ref.hpp b/apps/opencs/model/world/ref.hpp index 5d10a3a1b3..23b4ad1b5c 100644 --- a/apps/opencs/model/world/ref.hpp +++ b/apps/opencs/model/world/ref.hpp @@ -14,6 +14,7 @@ namespace CSMWorld std::string mCell; std::string mOriginalCell; bool mNew; // new reference, not counted yet, ref num not assigned yet + unsigned int mIdNum; CellRef(); diff --git a/apps/opencs/model/world/refcollection.cpp b/apps/opencs/model/world/refcollection.cpp index dfdb8e73bf..c6f5148290 100644 --- a/apps/opencs/model/world/refcollection.cpp +++ b/apps/opencs/model/world/refcollection.cpp @@ -7,8 +7,39 @@ #include "universalid.hpp" #include "record.hpp" +#include + +namespace CSMWorld +{ + template<> + void Collection >::removeRows (int index, int count) + { + mRecords.erase(mRecords.begin()+index, mRecords.begin()+index+count); + + // index map is updated in RefCollection::removeRows() + } + + template<> + void Collection >::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) + { + int size = static_cast(mRecords.size()); + if (index < 0 || index > size) + throw std::runtime_error("index out of range"); + + std::unique_ptr > record2(static_cast*>(record.release())); + + if (index == size) + mRecords.push_back(std::move(record2)); + else + mRecords.insert(mRecords.begin()+index, std::move(record2)); + + // index map is updated in RefCollection::insertRecord() + } +} + void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages) + std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord (cellIndex); @@ -19,8 +50,9 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool ESM::MovedCellRef mref; mref.mRefNum.mIndex = 0; bool isDeleted = false; + bool isMoved = false; - while (ESM::Cell::getNextRef(reader, ref, isDeleted, true, &mref)) + while (ESM::Cell::getNextRef(reader, ref, isDeleted, mref, isMoved)) { // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). @@ -34,7 +66,7 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool ref.mCell = "#" + std::to_string(index.first) + " " + std::to_string(index.second); // Handle non-base moved references - if (!base && mref.mRefNum.mIndex != 0) + if (!base && isMoved) { // Moved references must have a link back to their original cell // See discussion: https://forum.openmw.org/viewtopic.php?f=6&t=577&start=30 @@ -59,17 +91,47 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool else ref.mCell = cell2.mId; - mref.mRefNum.mIndex = 0; - - // ignore content file number - std::map::iterator iter = cache.begin(); - unsigned int thisIndex = ref.mRefNum.mIndex & 0x00ffffff; - if (ref.mRefNum.mContentFile != -1 && !base) ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; - - for (; iter != cache.end(); ++iter) + if (ref.mRefNum.mContentFile != -1 && !base) { - if (thisIndex == iter->first.mIndex) - break; + ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; + ref.mRefNum.mIndex &= 0x00ffffff; + } + + unsigned int refNum = (ref.mRefNum.mIndex & 0x00ffffff) | + (ref.mRefNum.hasContentFile() ? ref.mRefNum.mContentFile : 0xff) << 24; + + std::map::iterator iter = cache.find(refNum); + + if (isMoved) + { + if (iter == cache.end()) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, + mCells.getId(cellIndex)); + + messages.add(id, "Attempt to move a non-existent reference - RefNum index " + + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/"", + CSMDoc::Message::Severity_Warning); + continue; + } + + int index = getIntIndex(iter->second); + + // ensure we have the same record id for setRecord() + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId); + + std::unique_ptr > record(new Record); + // TODO: check whether a base record be moved + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); + + // overwrite original record + setRecord(index, std::move(record)); + + continue; // NOTE: assumed moved references are not deleted at the same time } if (isDeleted) @@ -79,13 +141,15 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Cell, mCells.getId (cellIndex)); - messages.add (id, "Attempt to delete a non-existent reference"); + messages.add (id, "Attempt to delete a non-existent reference - RefNum index " + + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID + ", content file index " + + std::to_string(ref.mRefNum.mContentFile), + /*hint*/"", + CSMDoc::Message::Severity_Warning); continue; } - int index = getIndex (iter->second); - - Record record = getRecord (index); + int index = getIntIndex (iter->second); if (base) { @@ -94,8 +158,9 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool } else { - record.mState = RecordBase::State_Deleted; - setRecord (index, record); + std::unique_ptr > record(new Record(getRecord(index))); + record->mState = RecordBase::State_Deleted; + setRecord(index, std::move(record)); } continue; @@ -104,30 +169,47 @@ void CSMWorld::RefCollection::load (ESM::ESMReader& reader, int cellIndex, bool if (iter==cache.end()) { // new reference + ref.mIdNum = mNextId; // FIXME: fragile ref.mId = getNewId(); - Record record; - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - const ESM::RefNum refNum = ref.mRefNum; - std::string refId = ref.mId; - (base ? record.mBase : record.mModified) = std::move(ref); + cache.emplace(refNum, ref.mIdNum); - appendRecord (record); + std::unique_ptr > record(new Record); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + (base ? record->mBase : record->mModified) = std::move(ref); - cache.emplace(refNum, std::move(refId)); + appendRecord(std::move(record)); } else { // old reference -> merge - ref.mId = iter->second; + int index = getIntIndex(iter->second); +#if 0 + // ref.mRefNum.mIndex : the key + // iter->second : previously cached idNum for the key + // index : position of the record for that idNum + // getRecord(index).get() : record in the index position + assert(iter->second != getRecord(index).get().mIdNum); // sanity check - int index = getIndex (ref.mId); + // check if the plugin used the same RefNum index for a different record + if (ref.mRefID != getRecord(index).get().mRefID) + { + CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); + messages.add(id, + "RefNum renamed from RefID \"" + getRecord(index).get().mRefID + "\" to \"" + + ref.mRefID + "\" (RefNum index " + std::to_string(ref.mRefNum.mIndex) + ")", + /*hint*/"", + CSMDoc::Message::Severity_Info); + } +#endif + ref.mId = getRecord(index).get().mId; + ref.mIdNum = extractIdNum(ref.mId); - Record record = getRecord (index); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; - (base ? record.mBase : record.mModified) = std::move(ref); + std::unique_ptr > record(new Record(getRecord(index))); + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; + (base ? record->mBase : record->mModified) = std::move(ref); - setRecord (index, record); + setRecord(index, std::move(record)); } } } @@ -136,3 +218,117 @@ std::string CSMWorld::RefCollection::getNewId() { return "ref#" + std::to_string(mNextId++); } + +unsigned int CSMWorld::RefCollection::extractIdNum(std::string_view id) const +{ + std::string::size_type separator = id.find_last_of('#'); + + if (separator == std::string::npos) + throw std::runtime_error("invalid ref ID: " + std::string(id)); + + return static_cast(std::stoi(std::string(id.substr(separator+1)))); +} + +int CSMWorld::RefCollection::getIntIndex (unsigned int id) const +{ + int index = searchId(id); + + if (index == -1) + throw std::runtime_error("invalid RefNum: " + std::to_string(id)); + + return index; +} + +int CSMWorld::RefCollection::searchId (unsigned int id) const +{ + std::map::const_iterator iter = mRefIndex.find(id); + + if (iter == mRefIndex.end()) + return -1; + + return iter->second; +} + +void CSMWorld::RefCollection::removeRows (int index, int count) +{ + Collection >::removeRows(index, count); // erase records only + + std::map::iterator iter = mRefIndex.begin(); + while (iter != mRefIndex.end()) + { + if (iter->second>=index) + { + if (iter->second >= index+count) + { + iter->second -= count; + ++iter; + } + else + mRefIndex.erase(iter++); + } + else + ++iter; + } +} + +void CSMWorld::RefCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) +{ + std::unique_ptr > record(new Record); + + record->mState = Record::State_ModifiedOnly; + record->mModified.blank(); + + record->get().mId = id; + record->get().mIdNum = extractIdNum(id); + + Collection >::appendRecord(std::move(record)); +} + +void CSMWorld::RefCollection::cloneRecord (const std::string& origin, + const std::string& destination, + const UniversalId::Type type) +{ + std::unique_ptr > copy(new Record); + + copy->mModified = getRecord(origin).get(); + copy->mState = RecordBase::State_ModifiedOnly; + + copy->get().mId = destination; + copy->get().mIdNum = extractIdNum(destination); + + insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() +} + +int CSMWorld::RefCollection::searchId(std::string_view id) const +{ + return searchId(extractIdNum(id)); +} + +void CSMWorld::RefCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) +{ + int index = getAppendIndex(/*id*/"", type); // for CellRef records id is ignored + + mRefIndex.insert(std::make_pair(static_cast*>(record.get())->get().mIdNum, index)); + + Collection >::insertRecord(std::move(record), index, type); // add records only +} + +void CSMWorld::RefCollection::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type) +{ + int size = getAppendIndex(/*id*/"", type); // for CellRef records id is ignored + unsigned int idNum = static_cast*>(record.get())->get().mIdNum; + + Collection >::insertRecord(std::move(record), index, type); // add records only + + if (index < size-1) + { + for (std::map::iterator iter(mRefIndex.begin()); iter != mRefIndex.end(); ++iter) + { + if (iter->second >= index) + ++(iter->second); + } + } + + mRefIndex.insert(std::make_pair(idNum, index)); +} diff --git a/apps/opencs/model/world/refcollection.hpp b/apps/opencs/model/world/refcollection.hpp index d031398d3f..affd66af32 100644 --- a/apps/opencs/model/world/refcollection.hpp +++ b/apps/opencs/model/world/refcollection.hpp @@ -2,6 +2,7 @@ #define CSM_WOLRD_REFCOLLECTION_H #include +#include #include "../doc/stage.hpp" @@ -14,12 +15,27 @@ namespace CSMWorld struct Cell; class UniversalId; + template<> + void Collection >::removeRows (int index, int count); + + template<> + void Collection >::insertRecord (std::unique_ptr record, int index, + UniversalId::Type type); + /// \brief References in cells class RefCollection : public Collection { Collection& mCells; + std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum + int mNextId; + unsigned int extractIdNum(std::string_view id) const; + + int getIntIndex (unsigned int id) const; + + int searchId (unsigned int id) const; + public: // MSVC needs the constructor for a class inheriting a template to be defined in header RefCollection (Collection& cells) @@ -27,10 +43,28 @@ namespace CSMWorld {} void load (ESM::ESMReader& reader, int cellIndex, bool base, - std::map& cache, CSMDoc::Messages& messages); + std::map& cache, CSMDoc::Messages& messages); ///< Load a sequence of references. std::string getNewId(); + + virtual void removeRows (int index, int count); + + virtual void appendBlankRecord (const std::string& id, + UniversalId::Type type = UniversalId::Type_None); + + virtual void cloneRecord (const std::string& origin, + const std::string& destination, + const UniversalId::Type type); + + virtual int searchId(std::string_view id) const; + + virtual void appendRecord (std::unique_ptr record, + UniversalId::Type type = UniversalId::Type_None); + + virtual void insertRecord (std::unique_ptr record, + int index, + UniversalId::Type type = UniversalId::Type_None); }; } diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index 1d1b5a94a6..84fec5bed0 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -25,6 +25,9 @@ namespace CSMWorld const RefIdColumn *mId; const RefIdColumn *mModified; const RefIdColumn *mType; + const RefIdColumn *mBlocked; + + BaseColumns () : mBlocked(nullptr) {} }; /// \brief Base adapter for all refereceable record types @@ -90,6 +93,9 @@ namespace CSMWorld if (column==mBase.mType) return static_cast (mType); + if (column==mBase.mBlocked) + return (record.get().mRecordFlags & ESM::FLAG_Blocked) != 0; + return QVariant(); } @@ -102,6 +108,17 @@ namespace CSMWorld if (column==mBase.mModified) record.mState = static_cast (value.toInt()); + else if (column==mBase.mBlocked) + { + RecordT record2 = record.get(); + + if (value.toInt() != 0) + record2.mRecordFlags |= ESM::FLAG_Blocked; + else + record2.mRecordFlags &= ~ESM::FLAG_Blocked; + + record.setModified(record2); + } } template @@ -110,6 +127,14 @@ namespace CSMWorld return mType; } + // NOTE: Body Part should not have persistence (but BodyPart is not listed in the Objects + // table at the moment). + // + // Spellmaking - not persistent - currently not part of objects table + // Enchanting - not persistent - currently not part of objects table + // + // Leveled Creature - no model, so not persistent + // Leveled Item - no model, so not persistent struct ModelColumns : public BaseColumns { diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 0cf9a4e6dc..928c7284ad 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -49,17 +50,22 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Blocked, ColumnBase::Display_Boolean, + ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); + baseColumns.mBlocked = &mColumns.back(); ModelColumns modelColumns (baseColumns); - mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); - modelColumns.mModel = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Persistent, ColumnBase::Display_Boolean); modelColumns.mPersistence = &mColumns.back(); + mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); + modelColumns.mModel = &mColumns.back(); NameColumns nameColumns (modelColumns); - mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String); + // Only items that can be placed in a container have the 32 character limit, but enforce + // that for all referenceable types for now. + mColumns.emplace_back(Columns::ColumnId_Name, ColumnBase::Display_String32); nameColumns.mName = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Script, ColumnBase::Display_Script); nameColumns.mScript = &mColumns.back(); @@ -231,9 +237,9 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiWanderRepeat, CSMWorld::ColumnBase::Display_Boolean)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String)); + new RefIdColumn (Columns::ColumnId_AiActivateName, CSMWorld::ColumnBase::Display_String32)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String)); + new RefIdColumn (Columns::ColumnId_AiTargetId, CSMWorld::ColumnBase::Display_String32)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_AiTargetCell, CSMWorld::ColumnBase::Display_String)); mColumns.back().addColumn( @@ -479,6 +485,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.emplace_back(Columns::ColumnId_Class, ColumnBase::Display_Class); npcColumns.mClass = &mColumns.back(); + // NAME32 enforced in IdCompletionDelegate::createEditor() mColumns.emplace_back(Columns::ColumnId_Faction, ColumnBase::Display_Faction); npcColumns.mFaction = &mColumns.back(); @@ -764,7 +771,6 @@ void CSMWorld::RefIdCollection::setNestedData(int row, int column, const QVarian const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedData(&mColumns.at (column), mData, localIndex.first, data, subRow, subColumn); - return; } void CSMWorld::RefIdCollection::removeRows (int index, int count) @@ -778,7 +784,6 @@ void CSMWorld::RefIdCollection::removeNestedRows(int row, int column, int subRow const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.removeNestedRow(&mColumns.at (column), mData, localIndex.first, subRow); - return; } void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, UniversalId::Type type) @@ -786,7 +791,7 @@ void CSMWorld::RefIdCollection::appendBlankRecord (const std::string& id, Univer mData.appendRecord (type, id, false); } -int CSMWorld::RefIdCollection::searchId (const std::string& id) const +int CSMWorld::RefIdCollection::searchId(std::string_view id) const { RefIdData::LocalIndex localIndex = mData.searchId (id); @@ -796,18 +801,18 @@ int CSMWorld::RefIdCollection::searchId (const std::string& id) const return mData.localToGlobalIndex (localIndex); } -void CSMWorld::RefIdCollection::replace (int index, const RecordBase& record) +void CSMWorld::RefIdCollection::replace (int index, std::unique_ptr record) { - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord (mData.globalToLocalIndex (index)).assign (*record.release()); } void CSMWorld::RefIdCollection::cloneRecord(const std::string& origin, const std::string& destination, const CSMWorld::UniversalId::Type type) { - std::unique_ptr newRecord(mData.getRecord(mData.searchId(origin)).modifiedCopy()); + std::unique_ptr newRecord = mData.getRecord(mData.searchId(origin)).modifiedCopy(); mAdapters.find(type)->second->setId(*newRecord, destination); - mData.insertRecord(*newRecord, type, destination); + mData.insertRecord(std::move(newRecord), type, destination); } bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) @@ -816,16 +821,16 @@ bool CSMWorld::RefIdCollection::touchRecord(const std::string& id) return false; } -void CSMWorld::RefIdCollection::appendRecord (const RecordBase& record, +void CSMWorld::RefIdCollection::appendRecord (std::unique_ptr record, UniversalId::Type type) { - std::string id = findAdapter (type).getId (record); + std::string id = findAdapter (type).getId (*record.get()); int index = mData.getAppendIndex (type); mData.appendRecord (type, id, false); - mData.getRecord (mData.globalToLocalIndex (index)).assign (record); + mData.getRecord (mData.globalToLocalIndex (index)).assign (*record.release()); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord (const std::string& id) const @@ -895,7 +900,6 @@ void CSMWorld::RefIdCollection::addNestedRow(int row, int col, int position) const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(col), localIndex.second); nestedAdapter.addNestedRow(&mColumns.at(col), mData, localIndex.first, position); - return; } void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) @@ -904,7 +908,6 @@ void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWor const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedTable(&mColumns.at(column), mData, localIndex.first, nestedTable); - return; } CSMWorld::NestedTableWrapperBase* CSMWorld::RefIdCollection::nestedTable(int row, int column) const diff --git a/apps/opencs/model/world/refidcollection.hpp b/apps/opencs/model/world/refidcollection.hpp index 5b38e80da5..ee17bb3214 100644 --- a/apps/opencs/model/world/refidcollection.hpp +++ b/apps/opencs/model/world/refidcollection.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "columnbase.hpp" #include "collectionbase.hpp" @@ -85,16 +86,16 @@ namespace CSMWorld void appendBlankRecord (const std::string& id, UniversalId::Type type) override; ///< \param type Will be ignored, unless the collection supports multiple record types - int searchId (const std::string& id) const override; + int searchId(std::string_view id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) - void replace (int index, const RecordBase& record) override; + void replace (int index, std::unique_ptr record) override; ///< If the record type does not match, an exception is thrown. /// /// \attention \a record must not change the ID. - void appendRecord (const RecordBase& record, UniversalId::Type type) override; + void appendRecord (std::unique_ptr record, UniversalId::Type type) override; ///< If the record type does not match, an exception is thrown. /// ///< \param type Will be ignored, unless the collection supports multiple record types diff --git a/apps/opencs/model/world/refiddata.cpp b/apps/opencs/model/world/refiddata.cpp index 79a2efdcc8..3bd8bfd5fc 100644 --- a/apps/opencs/model/world/refiddata.cpp +++ b/apps/opencs/model/world/refiddata.cpp @@ -2,6 +2,7 @@ #include #include +#include CSMWorld::RefIdDataContainerBase::~RefIdDataContainerBase() {} @@ -74,8 +75,7 @@ int CSMWorld::RefIdData::localToGlobalIndex (const LocalIndex& index) return globalIndex; } -CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId ( - const std::string& id) const +CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase (id); @@ -400,7 +400,7 @@ const CSMWorld::RefIdDataContainer< ESM::Static >& CSMWorld::RefIdData::getStati return mStatics; } -void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, const std::string& id) +void CSMWorld::RefIdData::insertRecord (std::unique_ptr record, CSMWorld::UniversalId::Type type, const std::string& id) { std::map::iterator iter = mRecordContainers.find (type); @@ -408,7 +408,7 @@ void CSMWorld::RefIdData::insertRecord (CSMWorld::RecordBase& record, CSMWorld:: if (iter==mRecordContainers.end()) throw std::logic_error ("invalid local index type"); - iter->second->insertRecord(record); + iter->second->insertRecord(std::move(record)); mIndex.insert (std::make_pair (Misc::StringUtils::lowerCase (id), LocalIndex (iter->second->getSize()-1, type))); @@ -420,9 +420,7 @@ void CSMWorld::RefIdData::copyTo (int index, RefIdData& target) const RefIdDataContainerBase *source = mRecordContainers.find (localIndex.second)->second; - std::string id = source->getId (localIndex.first); - - std::unique_ptr newRecord (source->getRecord (localIndex.first).modifiedCopy()); - - target.insertRecord (*newRecord, localIndex.second, id); + target.insertRecord(source->getRecord(localIndex.first).modifiedCopy(), + localIndex.second, + source->getId(localIndex.first)); } diff --git a/apps/opencs/model/world/refiddata.hpp b/apps/opencs/model/world/refiddata.hpp index ab5269b390..b9dee80638 100644 --- a/apps/opencs/model/world/refiddata.hpp +++ b/apps/opencs/model/world/refiddata.hpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include @@ -51,7 +54,7 @@ namespace CSMWorld virtual void appendRecord (const std::string& id, bool base) = 0; - virtual void insertRecord (RecordBase& record) = 0; + virtual void insertRecord (std::unique_ptr record) = 0; virtual int load (ESM::ESMReader& reader, bool base) = 0; ///< \return index of a loaded record or -1 if no record was loaded @@ -66,7 +69,7 @@ namespace CSMWorld template struct RefIdDataContainer : public RefIdDataContainerBase { - std::vector > mContainer; + std::vector > > mContainer; int getSize() const override; @@ -78,7 +81,7 @@ namespace CSMWorld void appendRecord (const std::string& id, bool base) override; - void insertRecord (RecordBase& record) override; + void insertRecord (std::unique_ptr record) override; int load (ESM::ESMReader& reader, bool base) override; ///< \return index of a loaded record or -1 if no record was loaded @@ -91,10 +94,13 @@ namespace CSMWorld }; template - void RefIdDataContainer::insertRecord(RecordBase& record) + void RefIdDataContainer::insertRecord(std::unique_ptr record) { - Record& newRecord = dynamic_cast& >(record); - mContainer.push_back(newRecord); + assert(record != nullptr); + // convert base pointer to record type pointer + std::unique_ptr> typedRecord(&dynamic_cast&>(*record)); + record.release(); + mContainer.push_back(std::move(typedRecord)); } template @@ -106,33 +112,33 @@ namespace CSMWorld template const RecordBase& RefIdDataContainer::getRecord (int index) const { - return mContainer.at (index); + return *mContainer.at (index); } template RecordBase& RefIdDataContainer::getRecord (int index) { - return mContainer.at (index); + return *mContainer.at (index); } template unsigned int RefIdDataContainer::getRecordFlags (int index) const { - return mContainer.at (index).get().mRecordFlags; + return mContainer.at (index)->get().mRecordFlags; } template void RefIdDataContainer::appendRecord (const std::string& id, bool base) { - Record record; + std::unique_ptr > record(new Record); - record.mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; + record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - record.mBase.mId = id; - record.mModified.mId = id; - (base ? record.mBase : record.mModified).blank(); + record->mBase.mId = id; + record->mModified.mId = id; + (base ? record->mBase : record->mModified).blank(); - mContainer.push_back (record); + mContainer.push_back (std::move(record)); } template @@ -147,7 +153,7 @@ namespace CSMWorld int numRecords = static_cast(mContainer.size()); for (; index < numRecords; ++index) { - if (Misc::StringUtils::ciEqual(mContainer[index].get().mId, record.mId)) + if (Misc::StringUtils::ciEqual(mContainer[index]->get().mId, record.mId)) { break; } @@ -165,7 +171,7 @@ namespace CSMWorld // Flag the record as Deleted even for a base content file. // RefIdData is responsible for its erasure. - mContainer[index].mState = RecordBase::State_Deleted; + mContainer[index]->mState = RecordBase::State_Deleted; } else { @@ -174,22 +180,22 @@ namespace CSMWorld appendRecord(record.mId, base); if (base) { - mContainer.back().mBase = record; + mContainer.back()->mBase = record; } else { - mContainer.back().mModified = record; + mContainer.back()->mModified = record; } } else if (!base) { - mContainer[index].setModified(record); + mContainer[index]->setModified(record); } else { // Overwrite - mContainer[index].setModified(record); - mContainer[index].merge(); + mContainer[index]->setModified(record); + mContainer[index]->merge(); } } @@ -208,13 +214,13 @@ namespace CSMWorld template std::string RefIdDataContainer::getId (int index) const { - return mContainer.at (index).get().mId; + return mContainer.at (index)->get().mId; } template void RefIdDataContainer::save (int index, ESM::ESMWriter& writer) const { - Record record = mContainer.at(index); + const Record& record = *mContainer.at(index); if (record.isModified() || record.mState == RecordBase::State_Deleted) { @@ -272,11 +278,11 @@ namespace CSMWorld int localToGlobalIndex (const LocalIndex& index) const; - LocalIndex searchId (const std::string& id) const; + LocalIndex searchId(std::string_view id) const; void erase (int index, int count); - void insertRecord (CSMWorld::RecordBase& record, CSMWorld::UniversalId::Type type, + void insertRecord (std::unique_ptr record, CSMWorld::UniversalId::Type type, const std::string& id); const RecordBase& getRecord (const LocalIndex& index) const; diff --git a/apps/opencs/model/world/resources.cpp b/apps/opencs/model/world/resources.cpp index c3eb9762e7..cd9f58e848 100644 --- a/apps/opencs/model/world/resources.cpp +++ b/apps/opencs/model/world/resources.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -22,10 +23,8 @@ void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const * size_t baseSize = mBaseDirectory.size(); - const std::map& index = vfs->getIndex(); - for (std::map::const_iterator it = index.begin(); it != index.end(); ++it) + for (const auto& filepath : vfs->getRecursiveDirectoryIterator("")) { - std::string filepath = it->first; if (filepath.size() #include #include +#include #include "universalid.hpp" @@ -35,7 +36,7 @@ namespace CSMWorld int getIndex (const std::string& id) const; - int searchId (const std::string& id) const; + int searchId(std::string_view id) const; UniversalId::Type getType() const; }; diff --git a/apps/opencs/view/doc/loader.cpp b/apps/opencs/view/doc/loader.cpp index 1cdfc01733..2420b87c6c 100644 --- a/apps/opencs/view/doc/loader.cpp +++ b/apps/opencs/view/doc/loader.cpp @@ -17,7 +17,7 @@ void CSVDoc::LoadingDocument::closeEvent (QCloseEvent *event) } CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) -: mDocument (document), mAborted (false), mMessages (nullptr), mTotalRecords (0) +: mDocument (document), mTotalRecordsLabel (0), mRecordsLabel (0), mAborted (false), mMessages (nullptr), mRecords(0) { setWindowTitle (QString::fromUtf8((std::string("Opening ") + document->getSavePath().filename().string()).c_str())); @@ -25,26 +25,25 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) mLayout = new QVBoxLayout (this); - // file progress - mFile = new QLabel (this); + // total progress + mTotalRecordsLabel = new QLabel (this); - mLayout->addWidget (mFile); + mLayout->addWidget (mTotalRecordsLabel); - mFileProgress = new QProgressBar (this); + mTotalProgress = new QProgressBar (this); - mLayout->addWidget (mFileProgress); + mLayout->addWidget (mTotalProgress); - int size = static_cast (document->getContentFiles().size())+1; - if (document->isNew()) - --size; + mTotalProgress->setMinimum (0); + mTotalProgress->setMaximum (document->getData().getTotalRecords(document->getContentFiles())); + mTotalProgress->setTextVisible (true); + mTotalProgress->setValue (0); + mTotalRecords = 0; - mFileProgress->setMinimum (0); - mFileProgress->setMaximum (size); - mFileProgress->setTextVisible (true); - mFileProgress->setValue (0); + mFilesLoaded = 0; // record progress - mLayout->addWidget (mRecords = new QLabel ("Records", this)); + mLayout->addWidget (mRecordsLabel = new QLabel ("Records", this)); mRecordProgress = new QProgressBar (this); @@ -74,29 +73,32 @@ CSVDoc::LoadingDocument::LoadingDocument (CSMDoc::Document *document) connect (mButtons, SIGNAL (rejected()), this, SLOT (cancel())); } -void CSVDoc::LoadingDocument::nextStage (const std::string& name, int totalRecords) +void CSVDoc::LoadingDocument::nextStage (const std::string& name, int fileRecords) { - mFile->setText (QString::fromUtf8 (("Loading: " + name).c_str())); + ++mFilesLoaded; + size_t numFiles = mDocument->getContentFiles().size(); - mFileProgress->setValue (mFileProgress->value()+1); + mTotalRecordsLabel->setText (QString::fromUtf8 (("Loading: "+name + +" ("+std::to_string(mFilesLoaded)+" of "+std::to_string((numFiles))+")").c_str())); + + mTotalRecords = mTotalProgress->value(); mRecordProgress->setValue (0); - mRecordProgress->setMaximum (totalRecords>0 ? totalRecords : 1); + mRecordProgress->setMaximum (fileRecords>0 ? fileRecords : 1); - mTotalRecords = totalRecords; + mRecords = fileRecords; } void CSVDoc::LoadingDocument::nextRecord (int records) { - if (records<=mTotalRecords) + if (records <= mRecords) { - mRecordProgress->setValue (records); + mTotalProgress->setValue (mTotalRecords+records); - std::ostringstream stream; + mRecordProgress->setValue(records); - stream << "Records: " << records << " of " << mTotalRecords; - - mRecords->setText (QString::fromUtf8 (stream.str().c_str())); + mRecordsLabel->setText(QString::fromStdString( + "Records: "+std::to_string(records)+" of "+std::to_string(mRecords))); } } @@ -176,12 +178,12 @@ void CSVDoc::Loader::loadingStopped (CSMDoc::Document *document, bool completed, } void CSVDoc::Loader::nextStage (CSMDoc::Document *document, const std::string& name, - int totalRecords) + int fileRecords) { std::map::iterator iter = mDocuments.find (document); if (iter!=mDocuments.end()) - iter->second->nextStage (name, totalRecords); + iter->second->nextStage (name, fileRecords); } void CSVDoc::Loader::nextRecord (CSMDoc::Document *document, int records) diff --git a/apps/opencs/view/doc/loader.hpp b/apps/opencs/view/doc/loader.hpp index 63b03ec8d2..1d21d1e02e 100644 --- a/apps/opencs/view/doc/loader.hpp +++ b/apps/opencs/view/doc/loader.hpp @@ -25,16 +25,18 @@ namespace CSVDoc Q_OBJECT CSMDoc::Document *mDocument; - QLabel *mFile; - QLabel *mRecords; - QProgressBar *mFileProgress; + QLabel *mTotalRecordsLabel; + QLabel *mRecordsLabel; + QProgressBar *mTotalProgress; QProgressBar *mRecordProgress; bool mAborted; QDialogButtonBox *mButtons; QLabel *mError; QListWidget *mMessages; QVBoxLayout *mLayout; + int mRecords; int mTotalRecords; + int mFilesLoaded; private: diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 4514cfb585..c89437d70d 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -65,6 +65,8 @@ void CSVDoc::View::setupFileMenu() QAction* save = createMenuEntry("Save", ":./menu-save.png", file, "document-file-save"); connect (save, SIGNAL (triggered()), this, SLOT (save())); mSave = save; + + file->addSeparator(); QAction* verify = createMenuEntry("Verify", ":./menu-verify.png", file, "document-file-verify"); connect (verify, SIGNAL (triggered()), this, SLOT (verify())); @@ -80,6 +82,8 @@ void CSVDoc::View::setupFileMenu() QAction* meta = createMenuEntry(CSMWorld::UniversalId::Type_MetaDatas, file, "document-file-metadata"); connect (meta, SIGNAL (triggered()), this, SLOT (addMetaDataSubView())); + file->addSeparator(); + QAction* close = createMenuEntry("Close", ":./menu-close.png", file, "document-file-close"); connect (close, SIGNAL (triggered()), this, SLOT (close())); @@ -156,17 +160,16 @@ void CSVDoc::View::setupWorldMenu() { QMenu *world = menuBar()->addMenu (tr ("World")); - QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); - connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); - - QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); - connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); - QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); connect (referenceables, SIGNAL (triggered()), this, SLOT (addReferenceablesSubView())); QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); connect (references, SIGNAL (triggered()), this, SLOT (addReferencesSubView())); + + world->addSeparator(); + + QAction* cells = createMenuEntry(CSMWorld::UniversalId::Type_Cells, world, "document-world-cells"); + connect (cells, SIGNAL (triggered()), this, SLOT (addCellsSubView())); QAction *lands = createMenuEntry(CSMWorld::UniversalId::Type_Lands, world, "document-world-lands"); connect (lands, SIGNAL (triggered()), this, SLOT (addLandsSubView())); @@ -177,7 +180,10 @@ void CSVDoc::View::setupWorldMenu() QAction *grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); connect (grid, SIGNAL (triggered()), this, SLOT (addPathgridSubView())); - world->addSeparator(); // items that don't represent single record lists follow here + world->addSeparator(); + + QAction* regions = createMenuEntry(CSMWorld::UniversalId::Type_Regions, world, "document-world-regions"); + connect (regions, SIGNAL (triggered()), this, SLOT (addRegionsSubView())); QAction *regionMap = createMenuEntry(CSMWorld::UniversalId::Type_RegionMap, world, "document-world-regionmap"); connect (regionMap, SIGNAL (triggered()), this, SLOT (addRegionMapSubView())); @@ -187,14 +193,19 @@ void CSVDoc::View::setupMechanicsMenu() { QMenu *mechanics = menuBar()->addMenu (tr ("Mechanics")); + QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); + connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); + connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); + QAction* globals = createMenuEntry(CSMWorld::UniversalId::Type_Globals, mechanics, "document-mechanics-globals"); connect (globals, SIGNAL (triggered()), this, SLOT (addGlobalsSubView())); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); connect (gmsts, SIGNAL (triggered()), this, SLOT (addGmstsSubView())); - - QAction* scripts = createMenuEntry(CSMWorld::UniversalId::Type_Scripts, mechanics, "document-mechanics-scripts"); - connect (scripts, SIGNAL (triggered()), this, SLOT (addScriptsSubView())); + + mechanics->addSeparator(); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); connect (spells, SIGNAL (triggered()), this, SLOT (addSpellsSubView())); @@ -204,9 +215,6 @@ void CSVDoc::View::setupMechanicsMenu() QAction* magicEffects = createMenuEntry(CSMWorld::UniversalId::Type_MagicEffects, mechanics, "document-mechanics-magiceffects"); connect (magicEffects, SIGNAL (triggered()), this, SLOT (addMagicEffectsSubView())); - - QAction* startScripts = createMenuEntry(CSMWorld::UniversalId::Type_StartScripts, mechanics, "document-mechanics-startscripts"); - connect (startScripts, SIGNAL (triggered()), this, SLOT (addStartScriptsSubView())); } void CSVDoc::View::setupCharacterMenu() @@ -227,21 +235,25 @@ void CSVDoc::View::setupCharacterMenu() QAction* birthsigns = createMenuEntry(CSMWorld::UniversalId::Type_Birthsigns, characters, "document-character-birthsigns"); connect (birthsigns, SIGNAL (triggered()), this, SLOT (addBirthsignsSubView())); + + QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); + connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); + characters->addSeparator(); + QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); connect (topics, SIGNAL (triggered()), this, SLOT (addTopicsSubView())); + QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); + connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); + + characters->addSeparator(); + QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); connect (journals, SIGNAL (triggered()), this, SLOT (addJournalsSubView())); - QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); - connect (topicInfos, SIGNAL (triggered()), this, SLOT (addTopicInfosSubView())); - QAction* journalInfos = createMenuEntry(CSMWorld::UniversalId::Type_JournalInfos, characters, "document-character-journalinfos"); connect (journalInfos, SIGNAL (triggered()), this, SLOT (addJournalInfosSubView())); - - QAction* bodyParts = createMenuEntry(CSMWorld::UniversalId::Type_BodyParts, characters, "document-character-bodyparts"); - connect (bodyParts, SIGNAL (triggered()), this, SLOT (addBodyPartsSubView())); } void CSVDoc::View::setupAssetsMenu() diff --git a/apps/opencs/view/render/actor.cpp b/apps/opencs/view/render/actor.cpp index d6077a65a5..271ca2365a 100644 --- a/apps/opencs/view/render/actor.cpp +++ b/apps/opencs/view/render/actor.cpp @@ -96,7 +96,7 @@ namespace CSVRender for (int i = 0; i < ESM::PRT_Count; ++i) { auto type = (ESM::PartReferenceType) i; - std::string partId = mActorData->getPart(type); + const std::string_view partId = mActorData->getPart(type); attachBodyPart(type, getBodyPartMesh(partId)); } } @@ -115,7 +115,7 @@ namespace CSVRender } } - std::string Actor::getBodyPartMesh(const std::string& bodyPartId) + std::string Actor::getBodyPartMesh(std::string_view bodyPartId) { const auto& bodyParts = mData.getBodyParts(); diff --git a/apps/opencs/view/render/actor.hpp b/apps/opencs/view/render/actor.hpp index 2f19454f78..8172e6fff7 100644 --- a/apps/opencs/view/render/actor.hpp +++ b/apps/opencs/view/render/actor.hpp @@ -2,6 +2,7 @@ #define OPENCS_VIEW_RENDER_ACTOR_H #include +#include #include @@ -54,7 +55,7 @@ namespace CSVRender void loadBodyParts(); void attachBodyPart(ESM::PartReferenceType, const std::string& mesh); - std::string getBodyPartMesh(const std::string& bodyPartId); + std::string getBodyPartMesh(std::string_view bodyPartId); static const std::string MeshPrefix; diff --git a/apps/opencs/view/render/brushdraw.cpp b/apps/opencs/view/render/brushdraw.cpp index 255a13a12e..6b33e336ea 100644 --- a/apps/opencs/view/render/brushdraw.cpp +++ b/apps/opencs/view/render/brushdraw.cpp @@ -18,6 +18,8 @@ CSVRender::BrushDraw::BrushDraw(osg::ref_ptr parentNode, bool textur mBrushDrawNode = new osg::Group(); mGeometry = new osg::Geometry(); mBrushDrawNode->addChild(mGeometry); + mBrushDrawNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + mBrushDrawNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); mParentNode->addChild(mBrushDrawNode); if (mTextureMode) mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_TEXTURE_SIZE); @@ -122,7 +124,14 @@ void CSVRender::BrushDraw::buildSquareGeometry(const float& radius, const osg::V const float brushOutlineHeight (1.0f); float diameter = radius * 2; int resolution = static_cast(2.f * diameter / mLandSizeFactor); //half a vertex resolution - float resAdjustedLandSizeFactor = mLandSizeFactor / 2; + float resAdjustedLandSizeFactor = mLandSizeFactor / 2; //128 + + if (resolution > 128) // limit accuracy for performance + { + resolution = 128; + resAdjustedLandSizeFactor = diameter / resolution; + } + osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); for (int i = 0; i < resolution; i++) @@ -215,7 +224,8 @@ void CSVRender::BrushDraw::buildCircleGeometry(const float& radius, const osg::V osg::ref_ptr geom (new osg::Geometry()); osg::ref_ptr vertices (new osg::Vec3Array()); osg::ref_ptr colors (new osg::Vec4Array()); - const int amountOfPoints = (osg::PI * 2.0f) * radius / 20; + + const int amountOfPoints = 128; const float step ((osg::PI * 2.0f) / static_cast(amountOfPoints)); const float brushOutlineHeight (1.0f); osg::Vec4f lineColor(1.0f, 1.0f, 1.0f, 0.6f); diff --git a/apps/opencs/view/render/cellarrow.cpp b/apps/opencs/view/render/cellarrow.cpp index a654171fce..776dbf7890 100644 --- a/apps/opencs/view/render/cellarrow.cpp +++ b/apps/opencs/view/render/cellarrow.cpp @@ -60,7 +60,7 @@ void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; - const int offset = cellSize / 2 + 800; + const int offset = cellSize / 2 + 600; int x = mCoordinates.getX()*cellSize + cellSize/2; int y = mCoordinates.getY()*cellSize + cellSize/2; @@ -92,9 +92,9 @@ void CSVRender::CellArrow::buildShape() { osg::ref_ptr geometry (new osg::Geometry); - const int arrowWidth = 4000; - const int arrowLength = 1500; - const int arrowHeight = 500; + const int arrowWidth = 2700; + const int arrowLength = 1350; + const int arrowHeight = 300; osg::Vec3Array *vertices = new osg::Vec3Array; for (int i2=0; i2<2; ++i2) diff --git a/apps/opencs/view/render/commands.cpp b/apps/opencs/view/render/commands.cpp index 7b37602961..699bf5d016 100644 --- a/apps/opencs/view/render/commands.cpp +++ b/apps/opencs/view/render/commands.cpp @@ -1,19 +1,44 @@ #include "commands.hpp" +#include + +#include #include +#include "editmode.hpp" #include "terrainselection.hpp" +#include "terrainshapemode.hpp" +#include "terraintexturemode.hpp" +#include "worldspacewidget.hpp" -CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand(TerrainSelection& terrainSelection, QUndoCommand* parent) - : mTerrainSelection(terrainSelection) +CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent) + : mWorldspaceWidget(worldspaceWidget) { } void CSVRender::DrawTerrainSelectionCommand::redo() { - mTerrainSelection.update(); + tryUpdate(); } void CSVRender::DrawTerrainSelectionCommand::undo() { - mTerrainSelection.update(); + tryUpdate(); +} + +void CSVRender::DrawTerrainSelectionCommand::tryUpdate() +{ + if (!mWorldspaceWidget) + { + Log(Debug::Verbose) << "Can't update terrain selection, no WorldspaceWidget found!"; + return; + } + + auto terrainMode = dynamic_cast(mWorldspaceWidget->getEditMode()); + if (!terrainMode) + { + Log(Debug::Verbose) << "Can't update terrain selection in current EditMode"; + return; + } + + terrainMode->getTerrainSelection()->update(); } diff --git a/apps/opencs/view/render/commands.hpp b/apps/opencs/view/render/commands.hpp index cdc389e33a..62b7fbfdcd 100644 --- a/apps/opencs/view/render/commands.hpp +++ b/apps/opencs/view/render/commands.hpp @@ -1,8 +1,12 @@ #ifndef CSV_RENDER_COMMANDS_HPP #define CSV_RENDER_COMMANDS_HPP +#include + #include +#include "worldspacewidget.hpp" + namespace CSVRender { class TerrainSelection; @@ -21,14 +25,17 @@ namespace CSVRender */ class DrawTerrainSelectionCommand : public QUndoCommand { + private: - TerrainSelection& mTerrainSelection; + QPointer mWorldspaceWidget; public: - DrawTerrainSelectionCommand(TerrainSelection& terrainSelection, QUndoCommand* parent = nullptr); + DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent = nullptr); void redo() override; void undo() override; + + void tryUpdate(); }; } diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 99ddce7f7d..ebb7f46fa5 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -297,6 +297,8 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) return false; } + mObjectsAtDragStart.clear(); + for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) { @@ -305,6 +307,12 @@ bool CSVRender::InstanceMode::primaryEditStartDrag (const QPoint& pos) if (mSubModeId == "move") { objectTag->mObject->setEdited (Object::Override_Position); + float x = objectTag->mObject->getPosition().pos[0]; + float y = objectTag->mObject->getPosition().pos[1]; + float z = objectTag->mObject->getPosition().pos[2]; + osg::Vec3f thisPoint(x, y, z); + mDragStart = getMousePlaneCoords(pos, getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart.emplace_back(thisPoint); mDragMode = DragMode_Move; } else if (mSubModeId == "rotate") @@ -392,29 +400,7 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); - if (mDragMode == DragMode_Move) - { - osg::Vec3f eye, centre, up; - getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt (eye, centre, up); - - if (diffY) - { - offset += up * diffY * speedFactor; - } - if (diffX) - { - offset += ((centre-eye) ^ up) * diffX * speedFactor; - } - - if (mDragAxis!=-1) - { - for (int i=0; i<3; ++i) - { - if (i!=mDragAxis) - offset[i] = 0; - } - } - } + if (mDragMode == DragMode_Move) {} else if (mDragMode == DragMode_Rotate) { osg::Vec3f eye, centre, up; @@ -514,17 +500,32 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou return; } + int i = 0; + // Apply - for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter) + for (std::vector >::iterator iter (selection.begin()); iter!=selection.end(); ++iter, i++) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { if (mDragMode == DragMode_Move) { ESM::Position position = objectTag->mObject->getPosition(); - for (int i=0; i<3; ++i) + osg::Vec3f mousePos = getMousePlaneCoords(pos, getProjectionSpaceCoords(mDragStart)); + float addToX = mousePos.x() - mDragStart.x(); + float addToY = mousePos.y() - mDragStart.y(); + float addToZ = mousePos.z() - mDragStart.z(); + position.pos[0] = mObjectsAtDragStart[i].x() + addToX; + position.pos[1] = mObjectsAtDragStart[i].y() + addToY; + position.pos[2] = mObjectsAtDragStart[i].z() + addToZ; + + // XYZ-locking + if (mDragAxis != -1) { - position.pos[i] += offset[i]; + for (int j = 0; j < 3; ++j) + { + if (j != mDragAxis) + position.pos[j] = mObjectsAtDragStart[i][j]; + } } objectTag->mObject->setPosition(position.pos); @@ -608,6 +609,7 @@ void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) } } + mObjectsAtDragStart.clear(); mDragMode = DragMode_None; } @@ -634,8 +636,10 @@ void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) std::vector > selection = getWorldspaceWidget().getEdited (Mask_Reference); + int j = 0; + for (std::vector >::iterator iter (selection.begin()); - iter!=selection.end(); ++iter) + iter!=selection.end(); ++iter, j++) { if (CSVRender::ObjectTag *objectTag = dynamic_cast (iter->get())) { @@ -643,6 +647,9 @@ void CSVRender::InstanceMode::dragWheel (int diff, double speedFactor) for (int i=0; i<3; ++i) position.pos[i] += offset[i]; objectTag->mObject->setPosition (position.pos); + osg::Vec3f thisPoint(position.pos[0], position.pos[1], position.pos[2]); + mDragStart = getMousePlaneCoords(getWorldspaceWidget().mapFromGlobal(QCursor::pos()), getProjectionSpaceCoords(thisPoint)); + mObjectsAtDragStart[j] = thisPoint; } } } diff --git a/apps/opencs/view/render/instancemode.hpp b/apps/opencs/view/render/instancemode.hpp index 73b7fff12a..4ece934e93 100644 --- a/apps/opencs/view/render/instancemode.hpp +++ b/apps/opencs/view/render/instancemode.hpp @@ -45,6 +45,8 @@ namespace CSVRender bool mLocked; float mUnitScaleDist; osg::ref_ptr mParentNode; + osg::Vec3f mDragStart; + std::vector mObjectsAtDragStart; int getSubModeFromId (const std::string& id) const; diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index d0e4dbe04a..789fad0587 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -682,18 +682,20 @@ void CSVRender::Object::apply (CSMWorld::CommandMacro& commands) int cellColumn = collection.findColumnIndex (static_cast ( CSMWorld::Columns::ColumnId_Cell)); - int refNumColumn = collection.findColumnIndex (static_cast ( - CSMWorld::Columns::ColumnId_RefNum)); + int origCellColumn = collection.findColumnIndex(static_cast ( + CSMWorld::Columns::ColumnId_OriginalCell)); if (cellIndex != originalIndex) { /// \todo figure out worldspace (not important until multiple worldspaces are supported) + std::string origCellId = CSMWorld::CellCoordinates(originalIndex).getId(""); std::string cellId = CSMWorld::CellCoordinates (cellIndex).getId (""); commands.push (new CSMWorld::ModifyCommand (*model, - model->index (recordIndex, cellColumn), QString::fromUtf8 (cellId.c_str()))); - commands.push (new CSMWorld::ModifyCommand( *model, - model->index (recordIndex, refNumColumn), 0)); + model->index (recordIndex, origCellColumn), QString::fromUtf8 (origCellId.c_str()))); + commands.push(new CSMWorld::ModifyCommand(*model, + model->index(recordIndex, cellColumn), QString::fromUtf8(cellId.c_str()))); + // NOTE: refnum is not modified for moving a reference to another cell } } diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index 866ff69cde..0504944954 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -287,7 +287,7 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() undoStack.beginMacro ("Edit shape and normal records"); // One command at the beginning of the macro for redrawing the terrain-selection grid when undoing the changes. - undoStack.push(new DrawTerrainSelectionCommand(*mTerrainShapeSelection)); + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); for(CSMWorld::CellCoordinates cellCoordinates: mAlteredCells) { @@ -358,7 +358,7 @@ void CSVRender::TerrainShapeMode::applyTerrainEditChanges() pushNormalsEditToCommand(landNormalsNew, document, landTable, cellId); } // One command at the end of the macro for redrawing the terrain-selection grid when redoing the changes. - undoStack.push(new DrawTerrainSelectionCommand(*mTerrainShapeSelection)); + undoStack.push(new DrawTerrainSelectionCommand(&getWorldspaceWidget())); undoStack.endMacro(); clearTransientEdits(); @@ -1049,7 +1049,7 @@ void CSVRender::TerrainShapeMode::handleSelection(int globalSelectionX, int glob */ if (xIsAtCellBorder && yIsAtCellBorder) { - /* + /* Handle the NW, NE, and SE corner vertices. NW corner: (+1, -1) offset to reach current cell. NE corner: (-1, -1) offset to reach current cell. @@ -1132,7 +1132,7 @@ void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString(); else selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString(); - + if (selectAction == "Select only") mTerrainShapeSelection->onlySelect(selections); else if (selectAction == "Add to selection") @@ -1444,6 +1444,11 @@ void CSVRender::TerrainShapeMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainShapeMode::getTerrainSelection() +{ + return mTerrainShapeSelection; +} + void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) { mBrushSize = brushSize; diff --git a/apps/opencs/view/render/terrainshapemode.hpp b/apps/opencs/view/render/terrainshapemode.hpp index a88e60c9c4..abc4b8aba8 100644 --- a/apps/opencs/view/render/terrainshapemode.hpp +++ b/apps/opencs/view/render/terrainshapemode.hpp @@ -92,6 +92,8 @@ namespace CSVRender void dragMoveEvent (QDragMoveEvent *event) override; void mouseMoveEvent (QMouseEvent *event) override; + std::shared_ptr getTerrainSelection(); + private: /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse @@ -176,7 +178,7 @@ namespace CSVRender int mDragMode = InteractionType_None; osg::Group* mParentNode; bool mIsEditing = false; - std::unique_ptr mTerrainShapeSelection; + std::shared_ptr mTerrainShapeSelection; int mTotalDiffY = 0; std::vector mAlteredCells; osg::Vec3d mEditingPos; diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index 4e267e9426..dfcc29ae01 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -712,6 +712,11 @@ void CSVRender::TerrainTextureMode::mouseMoveEvent (QMouseEvent *event) mBrushDraw->hide(); } +std::shared_ptr CSVRender::TerrainTextureMode::getTerrainSelection() +{ + return mTerrainTextureSelection; +} + void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) { diff --git a/apps/opencs/view/render/terraintexturemode.hpp b/apps/opencs/view/render/terraintexturemode.hpp index 31932df217..e0c6e4b40f 100644 --- a/apps/opencs/view/render/terraintexturemode.hpp +++ b/apps/opencs/view/render/terraintexturemode.hpp @@ -85,6 +85,8 @@ namespace CSVRender void mouseMoveEvent (QMouseEvent *event) override; + std::shared_ptr getTerrainSelection(); + private: /// \brief Handle brush mechanics, maths regarding worldspace hit etc. void editTerrainTextureGrid (const WorldspaceHitResult& hit); @@ -115,7 +117,7 @@ namespace CSVRender int mDragMode; osg::Group* mParentNode; bool mIsEditing; - std::unique_ptr mTerrainTextureSelection; + std::shared_ptr mTerrainTextureSelection; const int cellSize {ESM::Land::REAL_SIZE}; const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE}; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index b14b7953ab..c51479f897 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -452,6 +452,11 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo return hit; } +CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() +{ + return dynamic_cast (mEditMode->getCurrent()); +} + void CSVRender::WorldspaceWidget::abortDrag() { if (mDragging) @@ -697,11 +702,6 @@ void CSVRender::WorldspaceWidget::handleInteractionPress (const WorldspaceHitRes editMode.primaryOpenPressed (hit); } -CSVRender::EditMode *CSVRender::WorldspaceWidget::getEditMode() -{ - return dynamic_cast (mEditMode->getCurrent()); -} - void CSVRender::WorldspaceWidget::primaryOpen(bool activate) { handleInteraction(InteractionType_PrimaryOpen, activate); diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 5e224b3803..cf244ce712 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -189,6 +189,8 @@ namespace CSVRender /// Erase all overrides and restore the visual representation to its true state. virtual void reset (unsigned int elementMask) = 0; + EditMode *getEditMode(); + protected: /// Visual elements in a scene @@ -215,8 +217,6 @@ namespace CSVRender void settingChanged (const CSMPrefs::Setting *setting) override; - EditMode *getEditMode(); - bool getSpeedMode(); private: diff --git a/apps/opencs/view/tools/merge.hpp b/apps/opencs/view/tools/merge.hpp index d394a431ed..c7c4585979 100644 --- a/apps/opencs/view/tools/merge.hpp +++ b/apps/opencs/view/tools/merge.hpp @@ -1,5 +1,5 @@ -#ifndef CSV_TOOLS_REPORTTABLE_H -#define CSV_TOOLS_REPORTTABLE_H +#ifndef CSV_TOOLS_MERGE_H +#define CSV_TOOLS_MERGE_H #include diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index 07ba7907e7..d687cbeb3f 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -36,7 +36,7 @@ void CSVTools::SearchSubView::replace (bool selection) // in a single string. for (std::vector::const_reverse_iterator iter (indices.rbegin()); iter!=indices.rend(); ++iter) { - CSMWorld::UniversalId id = model.getUniversalId (*iter); + const CSMWorld::UniversalId& id = model.getUniversalId (*iter); CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType (id.getType()); diff --git a/apps/opencs/view/widget/scenetooltexturebrush.cpp b/apps/opencs/view/widget/scenetooltexturebrush.cpp index 272a5de42e..80eca21785 100644 --- a/apps/opencs/view/widget/scenetooltexturebrush.cpp +++ b/apps/opencs/view/widget/scenetooltexturebrush.cpp @@ -179,10 +179,10 @@ void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture) undoStack.endMacro(); } - if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) + if (index != -1 && !landtexturesCollection.getRecord(rowInNew).isDeleted()) { mBrushTextureLabel = "Selected texture: " + newBrushTextureId + " "; - mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value()); + mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(rowInNew, landTextureFilename).value()); } else { newBrushTextureId = ""; diff --git a/apps/opencs/view/world/cellcreator.cpp b/apps/opencs/view/world/cellcreator.cpp index 5b428a4b37..22c27c3d74 100644 --- a/apps/opencs/view/world/cellcreator.cpp +++ b/apps/opencs/view/world/cellcreator.cpp @@ -84,6 +84,13 @@ void CSVWorld::CellCreator::setType (int index) mYLabel->setVisible (index==1); mY->setVisible (index==1); + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + std::string text = mType->currentText().toStdString(); + if (text == "Interior Cell") + GenericCreator::setEditorMaxLength (64); + else + GenericCreator::setEditorMaxLength (32767); + update(); } diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index f2360b1378..152472f504 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -538,6 +538,9 @@ void CSVWorld::EditWidget::remake(int row) mainLayout->addLayout(tablesLayout, QSizePolicy::Preferred); mainLayout->addStretch(1); + int blockedColumn = mTable->searchColumnIndex(CSMWorld::Columns::ColumnId_Blocked); + bool isBlocked = mTable->data(mTable->index(row, blockedColumn)).toInt(); + int unlocked = 0; int locked = 0; const int columns = mTable->columnCount(); @@ -583,6 +586,8 @@ void CSVWorld::EditWidget::remake(int row) NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this, editable, fixedRows); table->resizeColumnsToContents(); + if (isBlocked) + table->setEditTriggers(QAbstractItemView::NoEditTriggers); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); @@ -617,7 +622,9 @@ void CSVWorld::EditWidget::remake(int row) label->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - if (! (mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable)) + // HACK: the blocked checkbox needs to keep the same position + // FIXME: unfortunately blocked record displays a little differently to unblocked one + if (!(mTable->flags (mTable->index (row, i)) & Qt::ItemIsEditable) || i == blockedColumn) { lockedLayout->addWidget (label, locked, 0); lockedLayout->addWidget (editor, locked, 1); @@ -639,7 +646,7 @@ void CSVWorld::EditWidget::remake(int row) createEditorContextMenu(editor, display, row); } } - else + else // Flag_Dialogue_List { CSMWorld::IdTree *tree = static_cast(mTable); mNestedTableMapper = new QDataWidgetMapper (this); @@ -686,7 +693,10 @@ void CSVWorld::EditWidget::remake(int row) label->setEnabled(false); } - createEditorContextMenu(editor, display, row); + if (!isBlocked) + createEditorContextMenu(editor, display, row); + else + editor->setEnabled(false); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 65ded46c7f..2681a398b2 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -78,6 +78,8 @@ QWidget *CSVWorld::EnumDelegate::createEditor(QWidget *parent, const QStyleOptio for (std::vector >::const_iterator iter (mValues.begin()); iter!=mValues.end(); ++iter) comboBox->addItem (iter->second); + + comboBox->setMaxVisibleItems(20); return comboBox; } diff --git a/apps/opencs/view/world/genericcreator.cpp b/apps/opencs/view/world/genericcreator.cpp index 23813f8066..8ae8f8764d 100644 --- a/apps/opencs/view/world/genericcreator.cpp +++ b/apps/opencs/view/world/genericcreator.cpp @@ -184,6 +184,11 @@ CSVWorld::GenericCreator::GenericCreator (CSMWorld::Data& data, QUndoStack& undo connect (&mData, SIGNAL (idListChanged()), this, SLOT (dataIdListChanged())); } +void CSVWorld::GenericCreator::setEditorMaxLength (int length) +{ + mId->setMaxLength (length); +} + void CSVWorld::GenericCreator::setEditLock (bool locked) { mLocked = locked; diff --git a/apps/opencs/view/world/genericcreator.hpp b/apps/opencs/view/world/genericcreator.hpp index 3e2a43c918..90c5946ae5 100644 --- a/apps/opencs/view/world/genericcreator.hpp +++ b/apps/opencs/view/world/genericcreator.hpp @@ -84,6 +84,8 @@ namespace CSVWorld std::string getNamespace() const; + void setEditorMaxLength(int length); + private: void updateNamespace(); diff --git a/apps/opencs/view/world/idcompletiondelegate.cpp b/apps/opencs/view/world/idcompletiondelegate.cpp index 447bcc25d9..9ef04ec3ab 100644 --- a/apps/opencs/view/world/idcompletiondelegate.cpp +++ b/apps/opencs/view/world/idcompletiondelegate.cpp @@ -81,6 +81,23 @@ QWidget *CSVWorld::IdCompletionDelegate::createEditor(QWidget *parent, CSMWorld::IdCompletionManager &completionManager = getDocument().getIdCompletionManager(); CSVWidget::DropLineEdit *editor = new CSVWidget::DropLineEdit(display, parent); editor->setCompleter(completionManager.getCompleter(display).get()); + + // The savegame format limits the player faction string to 32 characters. + // The region sound name is limited to 32 characters. (ESM::Region::SoundRef::mSound) + // The script name is limited to 32 characters. (ESM::Script::SCHD::mName) + // The cell name is limited to 64 characters. (ESM::Header::GMDT::mCurrentCell) + if (display == CSMWorld::ColumnBase::Display_Faction || + display == CSMWorld::ColumnBase::Display_Sound || + display == CSMWorld::ColumnBase::Display_Script || + display == CSMWorld::ColumnBase::Display_Referenceable) + { + editor->setMaxLength (32); + } + else if (display == CSMWorld::ColumnBase::Display_Cell) + { + editor->setMaxLength (64); + } + return editor; } diff --git a/apps/opencs/view/world/referenceablecreator.cpp b/apps/opencs/view/world/referenceablecreator.cpp index 836e8ac7dc..6bc0126b3e 100644 --- a/apps/opencs/view/world/referenceablecreator.cpp +++ b/apps/opencs/view/world/referenceablecreator.cpp @@ -22,7 +22,8 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); mType = new QComboBox (this); - + mType->setMaxVisibleItems(20); + for (std::vector::const_iterator iter (types.begin()); iter!=types.end(); ++iter) { @@ -31,8 +32,12 @@ CSVWorld::ReferenceableCreator::ReferenceableCreator (CSMWorld::Data& data, QUnd mType->addItem (QIcon (id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast (id2.getType())); } - + + mType->model()->sort(0); + insertBeforeButtons (mType, false); + + connect (mType, SIGNAL (currentIndexChanged (int)), this, SLOT (setType (int))); } void CSVWorld::ReferenceableCreator::reset() @@ -41,6 +46,30 @@ void CSVWorld::ReferenceableCreator::reset() GenericCreator::reset(); } +void CSVWorld::ReferenceableCreator::setType (int index) +{ + // container items have name limit of 32 characters + std::string text = mType->currentText().toStdString(); + if (text == "Potion" || + text == "Apparatus" || + text == "Armor" || + text == "Book" || + text == "Clothing" || + text == "Ingredient" || + text == "ItemLevelledList" || + text == "Light" || + text == "Lockpick" || + text == "Miscellaneous" || + text == "Probe" || + text == "Repair" || + text == "Weapon") + { + GenericCreator::setEditorMaxLength (32); + } + else + GenericCreator::setEditorMaxLength (32767); +} + void CSVWorld::ReferenceableCreator::cloneMode (const std::string& originId, const CSMWorld::UniversalId::Type type) { diff --git a/apps/opencs/view/world/referenceablecreator.hpp b/apps/opencs/view/world/referenceablecreator.hpp index d4657bcf7f..354347cc88 100644 --- a/apps/opencs/view/world/referenceablecreator.hpp +++ b/apps/opencs/view/world/referenceablecreator.hpp @@ -29,6 +29,9 @@ namespace CSVWorld void toggleWidgets(bool active = true) override; + private slots: + + void setType (int index); }; } diff --git a/apps/opencs/view/world/scenesubview.hpp b/apps/opencs/view/world/scenesubview.hpp index aabb7ca2a7..53cd54e7ac 100644 --- a/apps/opencs/view/world/scenesubview.hpp +++ b/apps/opencs/view/world/scenesubview.hpp @@ -32,7 +32,6 @@ namespace CSVWidget namespace CSVWorld { - class Table; class TableBottomBox; class CreatorFactoryBase; diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index c676a5ecc0..643396a057 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,7 @@ #include "../../model/prefs/shortcut.hpp" #include "tableeditidaction.hpp" +#include "tableheadermouseeventhandler.hpp" #include "util.hpp" void CSVWorld::Table::contextMenuEvent (QContextMenuEvent *event) @@ -239,7 +241,7 @@ void CSVWorld::Table::mouseDoubleClickEvent (QMouseEvent *event) CSVWorld::Table::Table (const CSMWorld::UniversalId& id, bool createAndDelete, bool sorting, CSMDoc::Document& document) : DragRecordTable(document), mCreateAction (nullptr), mCloneAction(nullptr), mTouchAction(nullptr), - mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false) + mRecordStatusDisplay (0), mJumpToAddedRecord(false), mUnselectAfterJump(false), mAutoJump (false) { mModel = &dynamic_cast (*mDocument.getData().getTableModel (id)); @@ -269,12 +271,6 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); - setSortingEnabled (sorting); - if (sorting) - { - sortByColumn (mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); - } - int columns = mModel->columnCount(); for (int i=0; ifindColumnIndex(CSMWorld::Columns::ColumnId_Id), Qt::AscendingOrder); + } + setSortingEnabled (sorting); + mEditAction = new QAction (tr ("Edit Record"), this); connect (mEditAction, SIGNAL (triggered()), this, SLOT (editRecord())); mEditAction->setIcon(QIcon(":edit-edit")); @@ -405,7 +408,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, /// \note This signal could instead be connected to a slot that filters out changes not affecting /// the records status column (for permanence reasons) connect (mProxyModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (tableSizeUpdate())); + this, SLOT (dataChangedEvent(const QModelIndex&, const QModelIndex&))); connect (selectionModel(), SIGNAL (selectionChanged (const QItemSelection&, const QItemSelection&)), this, SLOT (selectionSizeUpdate ())); @@ -420,6 +423,8 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id, connect (&CSMPrefs::State::get(), SIGNAL (settingChanged (const CSMPrefs::Setting *)), this, SLOT (settingChanged (const CSMPrefs::Setting *))); CSMPrefs::get()["ID Tables"].update(); + + new TableHeaderMouseEventHandler(this); } void CSVWorld::Table::setEditLock (bool locked) @@ -801,7 +806,7 @@ void CSVWorld::Table::tableSizeUpdate() case CSMWorld::RecordBase::State_BaseOnly: ++size; break; case CSMWorld::RecordBase::State_Modified: ++size; ++modified; break; case CSMWorld::RecordBase::State_ModifiedOnly: ++size; ++modified; break; - case CSMWorld::RecordBase:: State_Deleted: ++deleted; ++modified; break; + case CSMWorld::RecordBase::State_Deleted: ++deleted; ++modified; break; } } } @@ -874,15 +879,47 @@ std::vector< CSMWorld::UniversalId > CSVWorld::Table::getDraggedRecords() const return idToDrag; } +// parent, start and end depend on the model sending the signal, in this case mProxyModel +// +// If, for example, mModel was used instead, then scrolTo() should use the index +// mProxyModel->mapFromSource(mModel->index(end, 0)) void CSVWorld::Table::rowAdded(const std::string &id) { tableSizeUpdate(); if(mJumpToAddedRecord) { int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - selectRow(mProxyModel->getModelIndex(id, idColumn).row()); + int end = mProxyModel->getModelIndex(id, idColumn).row(); + selectRow(end); + + // without this delay the scroll works but goes to top for add/clone + QMetaObject::invokeMethod(this, "queuedScrollTo", Qt::QueuedConnection, Q_ARG(int, end)); if(mUnselectAfterJump) clearSelection(); } } + +void CSVWorld::Table::queuedScrollTo(int row) +{ + scrollTo(mProxyModel->index(row, 0), QAbstractItemView::PositionAtCenter); +} + +void CSVWorld::Table::dataChangedEvent(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + tableSizeUpdate(); + + if (mAutoJump) + { + selectRow(bottomRight.row()); + scrollTo(bottomRight, QAbstractItemView::PositionAtCenter); + } +} + +void CSVWorld::Table::jumpAfterModChanged(int state) +{ + if(state == Qt::Checked) + mAutoJump = true; + else + mAutoJump = false; +} diff --git a/apps/opencs/view/world/table.hpp b/apps/opencs/view/world/table.hpp index 9c4b9b5e5a..a7c932a383 100644 --- a/apps/opencs/view/world/table.hpp +++ b/apps/opencs/view/world/table.hpp @@ -74,6 +74,7 @@ namespace CSVWorld std::map mDoubleClickActions; bool mJumpToAddedRecord; bool mUnselectAfterJump; + bool mAutoJump; private: @@ -164,6 +165,12 @@ namespace CSVWorld void recordFilterChanged (std::shared_ptr filter); void rowAdded(const std::string &id); + + void dataChangedEvent(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void jumpAfterModChanged(int state); + + void queuedScrollTo(int state); }; } diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp new file mode 100644 index 0000000000..866c6149db --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -0,0 +1,64 @@ +#include "tableheadermouseeventhandler.hpp" +#include "dragrecordtable.hpp" + +#include +#include + +namespace CSVWorld +{ + +TableHeaderMouseEventHandler::TableHeaderMouseEventHandler(DragRecordTable * parent) + : QWidget(parent) + , table(*parent) + , header(*table.horizontalHeader()) +{ + header.setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect( + &header, &QHeaderView::customContextMenuRequested, [=](const QPoint & position) { showContextMenu(position); }); + + header.viewport()->installEventFilter(this); +} + +bool TableHeaderMouseEventHandler::eventFilter(QObject * tableWatched, QEvent * event) +{ + if (event->type() == QEvent::Type::MouseButtonPress) + { + auto & clickEvent = static_cast(*event); + if ((clickEvent.button() == Qt::MiddleButton)) + { + const auto & index = table.indexAt(clickEvent.pos()); + table.setColumnHidden(index.column(), true); + clickEvent.accept(); + return true; + } + } + return false; +} + +void TableHeaderMouseEventHandler::showContextMenu(const QPoint & position) +{ + auto & menu{createContextMenu()}; + menu.popup(header.viewport()->mapToGlobal(position)); +} + +QMenu & TableHeaderMouseEventHandler::createContextMenu() +{ + auto * menu = new QMenu(this); + for (int i = 0; i < table.model()->columnCount(); ++i) + { + const auto & name = table.model()->headerData(i, Qt::Horizontal, Qt::DisplayRole); + QAction * action{new QAction(name.toString(), this)}; + action->setCheckable(true); + action->setChecked(!table.isColumnHidden(i)); + menu->addAction(action); + + connect(action, &QAction::triggered, [=]() { + table.setColumnHidden(i, !action->isChecked()); + action->setChecked(!action->isChecked()); + action->toggle(); + }); + } + return *menu; +} + +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.hpp b/apps/opencs/view/world/tableheadermouseeventhandler.hpp new file mode 100644 index 0000000000..934bc1dbb7 --- /dev/null +++ b/apps/opencs/view/world/tableheadermouseeventhandler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace CSVWorld +{ +class DragRecordTable; + +class TableHeaderMouseEventHandler : public QWidget +{ +public: + explicit TableHeaderMouseEventHandler(DragRecordTable * parent); + + void showContextMenu(const QPoint &); + +private: + DragRecordTable & table; + QHeaderView & header; + + QMenu & createContextMenu(); + bool eventFilter(QObject *, QEvent *) override; + +}; // class TableHeaderMouseEventHandler +} // namespace CSVWorld diff --git a/apps/opencs/view/world/tablesubview.cpp b/apps/opencs/view/world/tablesubview.cpp index 5413f87a6e..6b4f12738e 100644 --- a/apps/opencs/view/world/tablesubview.cpp +++ b/apps/opencs/view/world/tablesubview.cpp @@ -1,5 +1,8 @@ #include "tablesubview.hpp" +#include +#include +#include #include #include #include @@ -18,7 +21,7 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) -: SubView (id) +: SubView (id), mShowOptions(false), mOptions(0) { QVBoxLayout *layout = new QVBoxLayout; @@ -30,7 +33,37 @@ CSVWorld::TableSubView::TableSubView (const CSMWorld::UniversalId& id, CSMDoc::D mFilterBox = new CSVFilter::FilterBox (document.getData(), this); - layout->insertWidget (0, mFilterBox); + QHBoxLayout *hLayout = new QHBoxLayout; + hLayout->insertWidget(0,mFilterBox); + + mOptions = new QWidget; + + QHBoxLayout *optHLayout = new QHBoxLayout; + QCheckBox *autoJump = new QCheckBox("Auto Jump"); + autoJump->setToolTip ("Whether to jump to the modified record." + "\nCan be useful in finding the moved or modified" + "\nobject instance while 3D editing."); + autoJump->setCheckState(Qt::Unchecked); + connect(autoJump, SIGNAL (stateChanged(int)), mTable, SLOT (jumpAfterModChanged(int))); + optHLayout->insertWidget(0, autoJump); + optHLayout->setContentsMargins (QMargins (0, 3, 0, 0)); + mOptions->setLayout(optHLayout); + mOptions->resize(mOptions->width(), mFilterBox->height()); + mOptions->hide(); + + QPushButton *opt = new QPushButton (); + opt->setIcon (QIcon (":startup/configure")); + opt->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed)); + opt->setToolTip ("Open additional options for this subview."); + connect (opt, SIGNAL (clicked()), this, SLOT (toggleOptions())); + + QVBoxLayout *buttonLayout = new QVBoxLayout; // work around margin issues + buttonLayout->setContentsMargins (QMargins (0/*left*/, 3/*top*/, 3/*right*/, 0/*bottom*/)); + buttonLayout->insertWidget(0, opt, 0, Qt::AlignVCenter|Qt::AlignRight); + hLayout->insertWidget(1, mOptions); + hLayout->insertLayout(2, buttonLayout); + + layout->insertLayout (0, hLayout); CSVDoc::SizeHintWidget *widget = new CSVDoc::SizeHintWidget; @@ -166,6 +199,20 @@ bool CSVWorld::TableSubView::eventFilter (QObject* object, QEvent* event) return false; } +void CSVWorld::TableSubView::toggleOptions() +{ + if (mShowOptions) + { + mShowOptions = false; + mOptions->hide(); + } + else + { + mShowOptions = true; + mOptions->show(); + } +} + void CSVWorld::TableSubView::requestFocus (const std::string& id) { mTable->requestFocus(id); diff --git a/apps/opencs/view/world/tablesubview.hpp b/apps/opencs/view/world/tablesubview.hpp index 337d2c7621..d89f71e8a4 100644 --- a/apps/opencs/view/world/tablesubview.hpp +++ b/apps/opencs/view/world/tablesubview.hpp @@ -6,6 +6,7 @@ #include class QModelIndex; +class QWidget; namespace CSMWorld { @@ -35,6 +36,8 @@ namespace CSVWorld Table *mTable; TableBottomBox *mBottom; CSVFilter::FilterBox *mFilterBox; + bool mShowOptions; + QWidget *mOptions; public: @@ -60,6 +63,7 @@ namespace CSVWorld void cloneRequest (const CSMWorld::UniversalId& toClone); void createFilterRequest(std::vector< CSMWorld::UniversalId >& types, Qt::DropAction action); + void toggleOptions (); public slots: diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index 58d3d49e44..80bd4580c8 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -291,6 +291,14 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return widget; } + case CSMWorld::ColumnBase::Display_String64: + { + // For other Display types (that represent record IDs) with drop support IdCompletionDelegate is used + CSVWidget::DropLineEdit *widget = new CSVWidget::DropLineEdit(display, parent); + widget->setMaxLength (64); + return widget; + } + default: return QStyledItemDelegate::createEditor (parent, option, index); diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3c29baf3aa..7ad9856a77 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -21,7 +21,7 @@ add_openmw_dir (mwrender actors objects renderingmanager animation rotatecontroller sky 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 + renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor ) add_openmw_dir (mwinput @@ -57,7 +57,8 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp actions object worldview userdataserializer eventqueue query - luabindings localscripts objectbindings cellbindings asyncbindings camerabindings uibindings + luabindings localscripts objectbindings cellbindings asyncbindings settingsbindings + camerabindings uibindings inputbindings nearbybindings ) add_openmw_dir (mwsound @@ -91,8 +92,8 @@ add_openmw_dir (mwmechanics drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects - spellabsorption linkedeffects + character actors objects aistate trading weaponpriority spellpriority weapontype spellutil + spellabsorption spelleffects ) add_openmw_dir (mwstate @@ -154,31 +155,6 @@ target_link_libraries(openmw ${LUA_LIBRARIES} ) -if(OSG_STATIC) - unset(_osg_plugins_static_files) - add_library(openmw_osg_plugins INTERFACE) - foreach(_plugin ${USED_OSG_PLUGINS}) - string(TOUPPER ${_plugin} _plugin_uc) - if(OPENMW_USE_SYSTEM_OSG) - list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) - else() - list(APPEND _osg_plugins_static_files $) - target_link_libraries(openmw_osg_plugins INTERFACE $) - add_dependencies(openmw_osg_plugins ${${_plugin_uc}_LIBRARY}) - endif() - endforeach() - # We use --whole-archive because OSG plugins use registration. - get_whole_archive_options(_opts ${_osg_plugins_static_files}) - target_link_options(openmw_osg_plugins INTERFACE ${_opts}) - target_link_libraries(openmw openmw_osg_plugins) - - if(OPENMW_USE_SYSTEM_OSG) - # OSG plugin pkgconfig files are missing these dependencies. - # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(openmw freetype jpeg png) - endif() -endif(OSG_STATIC) - if (ANDROID) target_link_libraries(openmw EGL android log z) endif (ANDROID) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 1bd79cdd02..bbe4f25f69 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -407,6 +407,7 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mExportFonts(false) , mRandomSeed(0) , mScriptContext (nullptr) + , mLuaManager (nullptr) , mFSStrict (false) , mScriptBlacklistUse (true) , mNewGame (false) @@ -427,7 +428,8 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::~Engine() { - mWorkQueue->stop(); + if (mScreenCaptureOperation != nullptr) + mScreenCaptureOperation->stop(); mEnvironment.cleanup(); @@ -519,7 +521,7 @@ std::string OMW::Engine::loadSettings (Settings::Manager & settings) throw std::runtime_error ("No default settings file found! Make sure the file \"defaults.bin\" was properly installed."); // load user settings if they exist - const std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); + std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); @@ -703,8 +705,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) mCfgMgr.getScreenshotPath().string(), Settings::Manager::getString("screenshot format", "General"), Settings::Manager::getBool("notify on saved screenshot", "General") - ? std::function(ScheduleNonDialogMessageBox {}) - : std::function(IgnoreString {}) + ? std::function(ScheduleNonDialogMessageBox {}) + : std::function(IgnoreString {}) ) ); @@ -751,6 +753,12 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) else gameControllerdb = ""; //if it doesn't exist, pass in an empty string + // gui needs our shaders path before everything else + mResourceSystem->getSceneManager()->setShaderPath((mResDir / "shaders").string()); + + osg::ref_ptr exts = osg::GLExtensions::Get(0, false); + bool shadersSupported = exts && (exts->glslLanguageVersion >= 1.2f); + std::string myguiResources = (mResDir / "mygui").string(); osg::ref_ptr guiRoot = new osg::Group; guiRoot->setName("GUI Root"); @@ -759,7 +767,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) MWGui::WindowManager* window = new MWGui::WindowManager(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), mCfgMgr.getLogPath().string() + std::string("/"), myguiResources, mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, - Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); + Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string(), shadersSupported); mEnvironment.setWindowManager (window); MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); @@ -867,7 +875,14 @@ public: void join() { if (mThread) + { + { + std::lock_guard lk(mMutex); + mJoinRequest = true; + } + mCV.notify_one(); mThread->join(); + } } private: @@ -883,10 +898,12 @@ private: void threadBody() { - while (!mEngine->mViewer->done() && !mEngine->mEnvironment.getStateManager()->hasQuitRequest()) + while (true) { std::unique_lock lk(mMutex); - mCV.wait(lk, [&]{ return mUpdateRequest; }); + mCV.wait(lk, [&]{ return mUpdateRequest || mJoinRequest; }); + if (mJoinRequest) + break; update(); @@ -899,7 +916,8 @@ private: Engine* mEngine; std::mutex mMutex; std::condition_variable mCV; - bool mUpdateRequest; + bool mUpdateRequest = false; + bool mJoinRequest = false; double mDt = 0; bool mIsGuiMode = false; std::optional mThread; diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 180e06bcbc..290fd890a6 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -21,6 +21,7 @@ namespace Resource namespace SceneUtil { class WorkQueue; + class AsyncScreenCaptureOperation; } namespace VFS @@ -67,7 +68,7 @@ namespace OMW boost::filesystem::path mResDir; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; - osg::ref_ptr mScreenCaptureOperation; + osg::ref_ptr mScreenCaptureOperation; std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index 324a18bdee..de0fb0df03 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -44,6 +44,10 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat desc.add_options() ("help", "print help message") ("version", "print version information and quit") + + ("replace", bpo::value()->default_value(Files::EscapeStringVector(), "") + ->multitoken()->composing(), "settings where the values from the current source should replace those from lower-priority sources instead of being appended") + ("data", bpo::value()->default_value(Files::EscapePathContainer(), "data") ->multitoken()->composing(), "set data directories (later directories have higher priority)") @@ -195,6 +199,15 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat Log(Debug::Error) << "No content file given (esm/esp, nor omwgame/omwaddon). Aborting..."; return false; } + std::set contentDedupe; + for (const auto& contentFile : content) + { + if (!contentDedupe.insert(contentFile).second) + { + Log(Debug::Error) << "Content file specified more than once: " << contentFile << ". Aborting..."; + return false; + } + } for (auto& file : content) { diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp index 6103921e02..dc1f6f9667 100644 --- a/apps/openmw/mwbase/dialoguemanager.hpp +++ b/apps/openmw/mwbase/dialoguemanager.hpp @@ -94,7 +94,6 @@ namespace MWBase virtual bool checkServiceRefused (ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; virtual void persuade (int type, ResponseCallback* callback) = 0; - virtual int getTemporaryDispositionChange () const = 0; /// @note Controlled by an option, gets discarded when dialogue ends by default virtual void applyBarterDispositionChange (int delta) = 0; diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 951b5053a2..5ac7c218b5 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace Loading @@ -51,9 +52,17 @@ namespace MWBase virtual void toggleControlSwitch (const std::string& sw, bool value) = 0; virtual bool getControlSwitch (const std::string& sw) = 0; - virtual std::string getActionDescription (int action) = 0; - virtual std::string getActionKeyBindingName (int action) = 0; - virtual std::string getActionControllerBindingName (int action) = 0; + virtual std::string getActionDescription (int action) const = 0; + virtual std::string getActionKeyBindingName (int action) const = 0; + virtual std::string getActionControllerBindingName (int action) const = 0; + virtual bool actionIsActive(int action) const = 0; + + virtual float getActionValue(int action) const = 0; // returns value in range [0, 1] + virtual 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; + ///Actions available for binding to keyboard buttons virtual std::vector getActionKeySorting() = 0; ///Actions available for binding to controller buttons @@ -74,6 +83,7 @@ namespace MWBase virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; virtual void resetIdleTime() = 0; + virtual bool isIdle() const = 0; virtual void executeAction(int action) = 0; diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 4e437246c4..ebcd8f50b3 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWBASE_LUAMANAGER_H #define GAME_MWBASE_LUAMANAGER_H +#include #include namespace MWWorld @@ -29,8 +30,6 @@ namespace MWBase virtual ~LuaManager() = default; virtual void newGameStarted() = 0; - virtual void keyPressed(const SDL_KeyboardEvent &arg) = 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; @@ -40,16 +39,23 @@ namespace MWBase // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, // const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) = 0; + struct InputEvent + { + enum {KeyPressed, KeyReleased, ControllerPressed, ControllerReleased, Action} mType; + std::variant mValue; + }; + virtual void inputEvent(const InputEvent& event) = 0; + struct ActorControls { - bool mDisableAI; - bool mControlledFromLua; + bool mDisableAI = false; + bool mChanged = false; - bool mJump; - bool mRun; - float mMovement; - float mSideMovement; - float mTurn; + bool mJump = false; + bool mRun = false; + float mMovement = 0; + float mSideMovement = 0; + float mTurn = 0; }; virtual ActorControls* getActorControls(const MWWorld::Ptr&) const = 0; diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index d6e948d685..6bedbb5b4d 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -58,7 +58,7 @@ namespace MWBase virtual void add (const MWWorld::Ptr& ptr) = 0; ///< Register an object for management - virtual void remove (const MWWorld::Ptr& ptr) = 0; + virtual void remove (const MWWorld::Ptr& ptr, bool keepActive) = 0; ///< Deregister an object for management virtual void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) = 0; @@ -100,7 +100,7 @@ namespace MWBase virtual int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) = 0; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. - virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) = 0; + virtual int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) = 0; ///< Calculate the diposition of an NPC toward the player. virtual int countDeaths (const std::string& id) const = 0; @@ -156,7 +156,7 @@ namespace MWBase PT_Bribe100, PT_Bribe1000 }; - virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) = 0; + virtual void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) = 0; ///< Perform a persuasion action on NPC virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0; @@ -277,8 +277,6 @@ namespace MWBase virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0; virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0; virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0; - - virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0; }; } diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index f256cc387e..961a63ac79 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -353,6 +353,8 @@ namespace MWBase virtual MWWorld::Ptr getWatchedActor() const = 0; virtual const std::string& getVersionDescription() const = 0; + + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 4110c8f489..8b33095fde 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -283,21 +283,20 @@ namespace MWBase virtual void deleteObject (const MWWorld::Ptr& ptr) = 0; virtual void undeleteObject (const MWWorld::Ptr& ptr) = 0; - virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false) = 0; + virtual MWWorld::Ptr moveObject (const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes - virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, float x, float y, float z, bool movePhysics=true) = 0; + virtual MWWorld::Ptr moveObject(const MWWorld::Ptr &ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) = 0; ///< @return an updated Ptr - virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) = 0; + virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr &ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) = 0; ///< @return an updated Ptr virtual void scaleObject (const MWWorld::Ptr& ptr, float scale) = 0; - virtual void rotateObject(const MWWorld::Ptr& ptr, float x, float y, float z, - RotationFlags flags = RotationFlag_inverseOrder) = 0; + virtual void rotateObject(const MWWorld::Ptr& ptr, const osg::Vec3f& rot, RotationFlags flags = RotationFlag_inverseOrder) = 0; - virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) = 0; + virtual MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) = 0; ///< Place an object. Makes a copy of the Ptr. virtual MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) = 0; @@ -540,7 +539,7 @@ namespace MWBase virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; - virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; + virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) = 0; virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0; virtual void updateProjectilesCasters() = 0; @@ -592,7 +591,7 @@ namespace MWBase virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, - const std::string& sourceName, const bool fromProjectile=false) = 0; + const std::string& sourceName, const bool fromProjectile=false, int slot = 0) = 0; virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; @@ -625,7 +624,7 @@ namespace MWBase virtual void setPlayerTraveling(bool traveling) = 0; virtual bool isPlayerTraveling() const = 0; - virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; + virtual void rotateWorldObject (const MWWorld::Ptr& ptr, const osg::Quat& rotate) = 0; /// Return terrain height at \a worldPos position. virtual float getTerrainHeightAt(const osg::Vec3f& worldPos) const = 0; @@ -654,7 +653,8 @@ namespace MWBase virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; + virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors = nullptr) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwclass/activator.cpp b/apps/openmw/mwclass/activator.cpp index 8ffcf70253..6285bdbf7e 100644 --- a/apps/openmw/mwclass/activator.cpp +++ b/apps/openmw/mwclass/activator.cpp @@ -38,15 +38,15 @@ namespace MWClass } } - void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Activator::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/activator.hpp b/apps/openmw/mwclass/activator.hpp index f7497f01bc..48a679e0b7 100644 --- a/apps/openmw/mwclass/activator.hpp +++ b/apps/openmw/mwclass/activator.hpp @@ -17,9 +17,9 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 6621df1ec8..ad43bd6e5f 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -22,7 +22,7 @@ namespace MWClass MWBase::Environment::get().getWorld()->adjustPosition(ptr, force); } - void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Actor::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if (!model.empty()) { diff --git a/apps/openmw/mwclass/actor.hpp b/apps/openmw/mwclass/actor.hpp index cc1d0c9089..886ffe4771 100644 --- a/apps/openmw/mwclass/actor.hpp +++ b/apps/openmw/mwclass/actor.hpp @@ -24,7 +24,7 @@ namespace MWClass ///< Adjust position to stand on ground. Must be called post model load /// @param force do this even if the ptr is flying - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 5c1ddd8bd1..06980f55da 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -106,15 +106,15 @@ namespace MWClass } } - void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Container::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/container.hpp b/apps/openmw/mwclass/container.hpp index 1cbc5a6142..0b290a73e1 100644 --- a/apps/openmw/mwclass/container.hpp +++ b/apps/openmw/mwclass/container.hpp @@ -42,8 +42,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 141bf99e92..54623e6699 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -1,5 +1,7 @@ #include "creature.hpp" +#include // INT_MIN + #include #include #include @@ -748,26 +750,35 @@ namespace MWClass if (!state.mHasCustomState) return; + const ESM::CreatureState& creatureState = state.asCreatureState(); + if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { - // Create a CustomData, but don't fill it from ESM records (not needed) - std::unique_ptr data (new CreatureCustomData); - - if (hasInventoryStore(ptr)) - data->mContainerStore = std::make_unique(); + // FIXME: the use of mGoldPool can be replaced with another flag the next time + // the save file format is changed + if (creatureState.mCreatureStats.mGoldPool == INT_MIN) + ensureCustomData(ptr); else - data->mContainerStore = std::make_unique(); + { + // Create a CustomData, but don't fill it from ESM records (not needed) + std::unique_ptr data (new CreatureCustomData); - ptr.getRefData().setCustomData (std::move(data)); + if (hasInventoryStore(ptr)) + data->mContainerStore = std::make_unique(); + else + data->mContainerStore = std::make_unique(); + + ptr.getRefData().setCustomData (std::move(data)); + } } } else ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); - const ESM::CreatureState& creatureState = state.asCreatureState(); + customData.mContainerStore->readState (creatureState.mInventory); bool spellsInitialised = customData.mCreatureStats.getSpells().setSpells(ptr.get()->mBase->mId); if(spellsInitialised) @@ -830,12 +841,12 @@ namespace MWClass } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index e608c27000..b5fe705ca6 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -55,9 +55,9 @@ namespace MWClass } } - void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) @@ -70,10 +70,10 @@ namespace MWClass } } - void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_Door); } bool Door::isDoor() const diff --git a/apps/openmw/mwclass/door.hpp b/apps/openmw/mwclass/door.hpp index 5882842e91..f9288a88ce 100644 --- a/apps/openmw/mwclass/door.hpp +++ b/apps/openmw/mwclass/door.hpp @@ -18,8 +18,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool isDoor() const override; diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index ad5eb4e737..69cc1a09bf 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -33,13 +33,13 @@ namespace MWClass renderingInterface.getObjects().insertModel(ptr, model, true, !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)); } - void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { MWWorld::LiveCellRef *ref = ptr.get(); assert (ref->mBase != nullptr); - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); if (!ref->mBase->mSound.empty() && !(ref->mBase->mData.mFlags & ESM::Light::OffDefault)) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, ref->mBase->mSound, 1.0, 1.0, @@ -47,11 +47,11 @@ namespace MWClass MWSound::PlayMode::Loop); } - void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Light::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { // TODO: add option somewhere to enable collision for placeable objects if (!model.empty() && (ptr.get()->mBase->mData.mFlags & ESM::Light::Carry) == 0) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } bool Light::useAnim() const diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 55fef759aa..e8aa4e5878 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -14,8 +14,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 5bf81caf9d..718b4d972d 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1,6 +1,7 @@ #include "npc.hpp" #include +#include // INT_MIN #include #include @@ -383,15 +384,15 @@ namespace MWClass if (!spellsInitialised) data->mNpcStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); - // inventory - // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items - data->mInventoryStore.fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); - data->mNpcStats.setGoldPool(gold); // store ptr.getRefData().setCustomData(std::move(data)); + // inventory + // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items + getInventoryStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId()); + getInventoryStore(ptr).autoEquip(ptr); } } @@ -1291,19 +1292,26 @@ namespace MWClass if (!state.mHasCustomState) return; + const ESM::NpcState& npcState = state.asNpcState(); + if (state.mVersion > 0) { if (!ptr.getRefData().getCustomData()) { - // Create a CustomData, but don't fill it from ESM records (not needed) - ptr.getRefData().setCustomData(std::make_unique()); + // FIXME: the use of mGoldPool can be replaced with another flag the next time + // the save file format is changed + if (npcState.mCreatureStats.mGoldPool == INT_MIN) + ensureCustomData(ptr); + else + // Create a CustomData, but don't fill it from ESM records (not needed) + ptr.getRefData().setCustomData(std::make_unique()); } } else ensureCustomData(ptr); // in openmw 0.30 savegames not all state was saved yet, so need to load it regardless. NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); - const ESM::NpcState& npcState = state.asNpcState(); + customData.mInventoryStore.readState (npcState.mInventory); customData.mNpcStats.readState (npcState.mNpcStats); bool spellsInitialised = customData.mNpcStats.getSpells().setSpells(ptr.get()->mBase->mId); @@ -1384,12 +1392,12 @@ namespace MWClass } MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); + MWBase::Environment::get().getWindowManager()->onDeleteCustomData(ptr); ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], - ptr.getCellRef().getPosition().pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } diff --git a/apps/openmw/mwclass/static.cpp b/apps/openmw/mwclass/static.cpp index 25029528ea..0805ca3dd1 100644 --- a/apps/openmw/mwclass/static.cpp +++ b/apps/openmw/mwclass/static.cpp @@ -23,15 +23,15 @@ namespace MWClass } } - void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { - insertObjectPhysics(ptr, model, rotation, physics, skipAnimated); + insertObjectPhysics(ptr, model, rotation, physics); } - void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { if(!model.empty()) - physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World, skipAnimated); + physics.addObject(ptr, model, rotation, MWPhysics::CollisionType_World); } std::string Static::getModel(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/static.hpp b/apps/openmw/mwclass/static.hpp index d1991d0550..c747eebf2f 100644 --- a/apps/openmw/mwclass/static.hpp +++ b/apps/openmw/mwclass/static.hpp @@ -14,8 +14,8 @@ namespace MWClass void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const override; ///< Add reference into a cell for rendering - void insertObject(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; - void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const override; + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; std::string getName (const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 3c6349ad85..8aa6acd8ed 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -52,8 +52,9 @@ namespace MWDialogue , mCompilerContext (MWScript::CompilerContext::Type_Dialogue) , mErrorHandler() , mTalkedTo(false) - , mTemporaryDispositionChange(0.f) - , mPermanentDispositionChange(0.f) + , mOriginalDisposition(0) + , mCurrentDisposition(0) + , mPermanentDispositionChange(0) { mChoice = -1; mIsInChoice = false; @@ -65,7 +66,8 @@ namespace MWDialogue { mKnownTopics.clear(); mTalkedTo = false; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; mPermanentDispositionChange = 0; } @@ -98,6 +100,20 @@ namespace MWDialogue } } + void DialogueManager::updateOriginalDisposition() + { + if(mActor.getClass().isNpc()) + { + const auto& stats = mActor.getClass().getNpcStats(mActor); + // Disposition changed by script; discard our preconceived notions + if(stats.getBaseDisposition() != mCurrentDisposition) + { + mCurrentDisposition = stats.getBaseDisposition(); + mOriginalDisposition = mCurrentDisposition; + } + } + } + bool DialogueManager::startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) { updateGlobals(); @@ -107,8 +123,7 @@ namespace MWDialogue return false; mLastTopic = ""; - mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + // Note that we intentionally don't reset mPermanentDispositionChange mChoice = -1; mIsInChoice = false; @@ -398,19 +413,21 @@ namespace MWDialogue void DialogueManager::goodbyeSelected() { - // Apply disposition change to NPC's base disposition - if (mActor.getClass().isNpc()) + // Apply disposition change to NPC's base disposition if we **think** we need to change something + if ((mPermanentDispositionChange || mOriginalDisposition != mCurrentDisposition) && mActor.getClass().isNpc()) { - // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mPermanentDispositionChange < 0) - mPermanentDispositionChange = -curDisp; - + updateOriginalDisposition(); MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); - npcStats.setBaseDisposition(static_cast(npcStats.getBaseDisposition() + mPermanentDispositionChange)); + // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with intimidate) + npcStats.setBaseDisposition(0); + int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); + int disposition = std::min(100 - zero, std::max(mOriginalDisposition + mPermanentDispositionChange, -zero)); + + npcStats.setBaseDisposition(disposition); } mPermanentDispositionChange = 0; - mTemporaryDispositionChange = 0; + mOriginalDisposition = 0; + mCurrentDisposition = 0; } void DialogueManager::questionAnswered (int answer, ResponseCallback* callback) @@ -490,20 +507,17 @@ namespace MWDialogue void DialogueManager::persuade(int type, ResponseCallback* callback) { bool success; - float temp, perm; + int temp, perm; MWBase::Environment::get().getMechanicsManager()->getPersuasionDispositionChange( mActor, MWBase::MechanicsManager::PersuasionType(type), success, temp, perm); - mTemporaryDispositionChange += temp; + updateOriginalDisposition(); + if(temp > 0 && perm > 0 && mOriginalDisposition + perm + mPermanentDispositionChange < 0) + perm = -(mOriginalDisposition + mPermanentDispositionChange); + mCurrentDisposition += temp; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); mPermanentDispositionChange += perm; - // change temp disposition so that final disposition is between 0...100 - float curDisp = static_cast(MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false)); - if (curDisp + mTemporaryDispositionChange < 0) - mTemporaryDispositionChange = -curDisp; - else if (curDisp + mTemporaryDispositionChange > 100) - mTemporaryDispositionChange = 100 - curDisp; - MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().skillUsageSucceeded(player, ESM::Skill::Speechcraft, success ? 0 : 1); @@ -539,16 +553,16 @@ namespace MWDialogue executeTopic (text + (success ? " Success" : " Fail"), callback); } - int DialogueManager::getTemporaryDispositionChange() const - { - return static_cast(mTemporaryDispositionChange); - } - void DialogueManager::applyBarterDispositionChange(int delta) { - mTemporaryDispositionChange += delta; - if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) - mPermanentDispositionChange += delta; + if(mActor.getClass().isNpc()) + { + updateOriginalDisposition(); + mCurrentDisposition += delta; + mActor.getClass().getNpcStats(mActor).setBaseDisposition(mCurrentDisposition); + if (Settings::Manager::getBool("barter disposition change is permanent", "Game")) + mPermanentDispositionChange += delta; + } } bool DialogueManager::checkServiceRefused(ResponseCallback* callback, ServiceType service) diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp index b35bee6d43..ab2625ff5a 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp @@ -47,8 +47,9 @@ namespace MWDialogue std::vector > mChoices; - float mTemporaryDispositionChange; - float mPermanentDispositionChange; + int mOriginalDisposition; + int mCurrentDisposition; + int mPermanentDispositionChange; void parseText (const std::string& text); @@ -62,6 +63,8 @@ namespace MWDialogue const ESM::Dialogue* searchDialogue(const std::string& id); + void updateOriginalDisposition(); + public: DialogueManager (const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); @@ -96,7 +99,6 @@ namespace MWDialogue void questionAnswered (int answer, ResponseCallback* callback) override; void persuade (int type, ResponseCallback* callback) override; - int getTemporaryDispositionChange () const override; /// @note Controlled by an option, gets discarded when dialogue ends by default void applyBarterDispositionChange (int delta) override; diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index f296f223fb..8ba3349bc8 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -1,8 +1,9 @@ #ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H -#include #include +#include +#include #include #include #include // std::reverse @@ -68,6 +69,19 @@ public: return false; } + static bool isWhitespaceUTF8(const int utf8Char) + { + if (utf8Char >= 0 && utf8Char <= static_cast( std::numeric_limits::max())) + { + //That function has undefined behavior if the character doesn't fit in unsigned char + return std::isspace(utf8Char); + } + else + { + return false; + } + } + static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; @@ -83,7 +97,7 @@ public: { Point prev = i; --prev; - if(isalpha(*prev)) + if(!isWhitespaceUTF8(*prev)) continue; } diff --git a/apps/openmw/mwgui/bookpage.cpp b/apps/openmw/mwgui/bookpage.cpp index 49fae04619..874d1d866b 100644 --- a/apps/openmw/mwgui/bookpage.cpp +++ b/apps/openmw/mwgui/bookpage.cpp @@ -8,6 +8,7 @@ #include "MyGUI_FactoryManager.h" #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -107,7 +108,7 @@ struct TypesetBookImpl : TypesetBook virtual ~TypesetBookImpl () {} - Range addContent (BookTypesetter::Utf8Span text) + Range addContent (const BookTypesetter::Utf8Span &text) { Contents::iterator i = mContents.insert (mContents.end (), Content (text.first, text.second)); @@ -490,7 +491,8 @@ struct TypesetBookImpl::Typesetter : BookTypesetter { add_partial_text(); stream.consume (); - mLine = nullptr, mRun = nullptr; + mLine = nullptr; + mRun = nullptr; continue; } @@ -550,7 +552,9 @@ struct TypesetBookImpl::Typesetter : BookTypesetter if (left + space_width + word_width > mPageWidth) { - mLine = nullptr, mRun = nullptr, left = 0; + mLine = nullptr; + mRun = nullptr; + left = 0; } else { @@ -1217,8 +1221,10 @@ public: RenderXform renderXform (mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); + float z = SceneUtil::getReverseZ() ? 1.f : -1.f; + GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), - -1 /*mNode->getNodeDepth()*/, vertices, renderXform); + z /*mNode->getNodeDepth()*/, vertices, renderXform); int visit_top = (std::max) (mViewTop, mViewTop + int (renderXform.clipTop )); int visit_bottom = (std::min) (mViewBottom, mViewTop + int (renderXform.clipBottom)); diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 827f87c7d6..43da1ef83f 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -72,13 +72,6 @@ namespace return {question, {r2, r1, r0}, sound}; } } - - void updatePlayerHealth() - { - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player); - npcStats.updateHealth(); - } } namespace MWGui @@ -372,8 +365,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog); mPickClassDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) @@ -448,8 +439,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog); mRaceDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onRaceDialogBack() @@ -477,8 +466,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog); mBirthSignDialog = nullptr; } - - updatePlayerHealth(); } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) @@ -527,7 +514,6 @@ namespace MWGui // Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later mCreateClassDialog->setVisible(false); } - updatePlayerHealth(); } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) @@ -719,8 +705,6 @@ namespace MWGui MWBase::Environment::get().getWorld()->getStore().get().find(mGenerateClass); mPlayerClass = *klass; - - updatePlayerHealth(); } void CharacterCreation::onGenerateClassBack() diff --git a/apps/openmw/mwgui/class.cpp b/apps/openmw/mwgui/class.cpp index ee5fe59399..1732bc24ea 100644 --- a/apps/openmw/mwgui/class.cpp +++ b/apps/openmw/mwgui/class.cpp @@ -517,6 +517,7 @@ namespace MWGui std::vector CreateClassDialog::getMajorSkills() const { std::vector v; + v.reserve(5); for(int i = 0; i < 5; i++) { v.push_back(mMajorSkill[i]->getSkillId()); @@ -527,6 +528,7 @@ namespace MWGui std::vector CreateClassDialog::getMinorSkills() const { std::vector v; + v.reserve(5); for(int i=0; i < 5; i++) { v.push_back(mMinorSkill[i]->getSkillId()); diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 4cdd8b137f..de771051ef 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -262,7 +262,7 @@ namespace MWGui } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second); creatureMap.clear(); @@ -277,8 +277,9 @@ namespace MWGui auto it = std::find_if(summons.begin(), summons.end(), [&] (const auto& entry) { return entry.second == creatureStats.getActorId(); }); if(it != summons.end()) { - MWMechanics::purgeSummonEffect(summoner, *it); + auto summon = *it; summons.erase(it); + MWMechanics::purgeSummonEffect(summoner, summon); break; } } @@ -302,4 +303,9 @@ namespace MWGui return mModel->onTakeItem(item.mBase, count); } + void ContainerWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + if(mModel && mModel->usesContainer(ptr)) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); + } } diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 85c0dddc67..2a0dee44e2 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -35,6 +35,8 @@ namespace MWGui void resetReference() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + private: DragAndDrop* mDragAndDrop; diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index 56f084bb9d..81dde1c059 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -86,7 +86,7 @@ size_t ContainerItemModel::getItemCount() return mItems.size(); } -ItemModel::ModelIndex ContainerItemModel::getIndex (ItemStack item) +ItemModel::ModelIndex ContainerItemModel::getIndex (const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) @@ -249,4 +249,14 @@ bool ContainerItemModel::onTakeItem(const MWWorld::Ptr &item, int count) return true; } +bool ContainerItemModel::usesContainer(const MWWorld::Ptr& container) +{ + for(const auto& source : mItemSources) + { + if(source.first == container) + return true; + } + return false; +} + } diff --git a/apps/openmw/mwgui/containeritemmodel.hpp b/apps/openmw/mwgui/containeritemmodel.hpp index c54f113147..11fed06913 100644 --- a/apps/openmw/mwgui/containeritemmodel.hpp +++ b/apps/openmw/mwgui/containeritemmodel.hpp @@ -28,7 +28,7 @@ namespace MWGui bool onTakeItem(const MWWorld::Ptr &item, int count) override; ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; size_t getItemCount() override; MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; @@ -36,6 +36,8 @@ namespace MWGui void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + private: std::vector> mItemSources; std::vector mWorldItems; diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 45defe9a56..5a7b5a9590 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -53,10 +53,11 @@ namespace MWGui } void removeItem (const ItemStack& item, size_t count) override { throw std::runtime_error("removeItem not implemented"); } - ModelIndex getIndex (ItemStack item) override { throw std::runtime_error("getIndex not implemented"); } + ModelIndex getIndex (const ItemStack &item) override { throw std::runtime_error("getIndex not implemented"); } void update() override {} size_t getItemCount() override { return 0; } ItemStack getItem (ModelIndex index) override { throw std::runtime_error("getItem not implemented"); } + bool usesContainer(const MWWorld::Ptr&) override { return false; } private: // Where to drop the item diff --git a/apps/openmw/mwgui/inventoryitemmodel.cpp b/apps/openmw/mwgui/inventoryitemmodel.cpp index f2ff64aa16..f96cfd865e 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.cpp +++ b/apps/openmw/mwgui/inventoryitemmodel.cpp @@ -34,7 +34,7 @@ size_t InventoryItemModel::getItemCount() return mItems.size(); } -ItemModel::ModelIndex InventoryItemModel::getIndex (ItemStack item) +ItemModel::ModelIndex InventoryItemModel::getIndex (const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) @@ -131,4 +131,9 @@ bool InventoryItemModel::onTakeItem(const MWWorld::Ptr &item, int count) return true; } +bool InventoryItemModel::usesContainer(const MWWorld::Ptr& container) +{ + return mActor == container; +} + } diff --git a/apps/openmw/mwgui/inventoryitemmodel.hpp b/apps/openmw/mwgui/inventoryitemmodel.hpp index 30d17f3e6c..87d874c946 100644 --- a/apps/openmw/mwgui/inventoryitemmodel.hpp +++ b/apps/openmw/mwgui/inventoryitemmodel.hpp @@ -12,7 +12,7 @@ namespace MWGui InventoryItemModel (const MWWorld::Ptr& actor); ItemStack getItem (ModelIndex index) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; size_t getItemCount() override; bool onTakeItem(const MWWorld::Ptr &item, int count) override; @@ -25,6 +25,8 @@ namespace MWGui void update() override; + bool usesContainer(const MWWorld::Ptr& container) override; + protected: MWWorld::Ptr mActor; private: diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 5f18fba91f..f53ba21f9f 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -29,6 +29,7 @@ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/npcstats.hpp" #include "itemview.hpp" #include "inventoryitemmodel.hpp" @@ -703,7 +704,7 @@ namespace MWGui if (!MWBase::Environment::get().getWindowManager()->isAllowed(GW_Inventory)) return; // make sure the object is of a type that can be picked up - std::string type = object.getTypeName(); + const std::string& type = object.getTypeName(); if ( (type != typeid(ESM::Apparatus).name()) && (type != typeid(ESM::Armor).name()) && (type != typeid(ESM::Book).name()) @@ -732,6 +733,12 @@ namespace MWGui if (!object.getRefData().activate()) return; + // Player must not be paralyzed, knocked down, or dead to pick up an item. + const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); + bool godmode = MWBase::Environment::get().getWorld()->getGodModeState(); + if ((!godmode && playerStats.isParalyzed()) || playerStats.getKnockedDown() || playerStats.isDead()) + return; + MWBase::Environment::get().getMechanicsManager()->itemTaken(player, object, MWWorld::Ptr(), count); // add to player inventory diff --git a/apps/openmw/mwgui/itemmodel.cpp b/apps/openmw/mwgui/itemmodel.cpp index 4e4d77da47..7f74d3fb5a 100644 --- a/apps/openmw/mwgui/itemmodel.cpp +++ b/apps/openmw/mwgui/itemmodel.cpp @@ -129,7 +129,7 @@ namespace MWGui return -1; } - ItemModel::ModelIndex ProxyItemModel::getIndex (ItemStack item) + ItemModel::ModelIndex ProxyItemModel::getIndex (const ItemStack& item) { return mSourceModel->getIndex(item); } @@ -162,4 +162,9 @@ namespace MWGui { return mSourceModel->onTakeItem(item, count); } + + bool ProxyItemModel::usesContainer(const MWWorld::Ptr& container) + { + return mSourceModel->usesContainer(container); + } } diff --git a/apps/openmw/mwgui/itemmodel.hpp b/apps/openmw/mwgui/itemmodel.hpp index e120dde0fa..d538a040db 100644 --- a/apps/openmw/mwgui/itemmodel.hpp +++ b/apps/openmw/mwgui/itemmodel.hpp @@ -55,7 +55,7 @@ namespace MWGui virtual size_t getItemCount() = 0; /// Returns an invalid index if the item was not found - virtual ModelIndex getIndex (ItemStack item) = 0; + virtual ModelIndex getIndex (const ItemStack &item) = 0; /// Rebuild the item model, this will invalidate existing model indices virtual void update() = 0; @@ -75,6 +75,8 @@ namespace MWGui virtual bool onDropItem(const MWWorld::Ptr &item, int count); virtual bool onTakeItem(const MWWorld::Ptr &item, int count); + virtual bool usesContainer(const MWWorld::Ptr& container) = 0; + private: ItemModel(const ItemModel&); ItemModel& operator=(const ItemModel&); @@ -96,13 +98,15 @@ namespace MWGui MWWorld::Ptr copyItem (const ItemStack& item, size_t count, bool allowAutoEquip = true) override; void removeItem (const ItemStack& item, size_t count) override; - ModelIndex getIndex (ItemStack item) override; + ModelIndex getIndex (const ItemStack &item) override; /// @note Takes ownership of the passed pointer. void setSourceModel(ItemModel* sourceModel); ModelIndex mapToSource (ModelIndex index); ModelIndex mapFromSource (ModelIndex index); + + bool usesContainer(const MWWorld::Ptr& container) override; protected: ItemModel* mSourceModel; }; diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index cc793073e3..a029fe54b6 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -82,10 +82,7 @@ namespace MWGui MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison - for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells()) - { - spell.second.mNextWorsening += mDays * 24; - } + player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); std::set skills; for (int day=0; day #include +#include #include #include #include @@ -66,35 +67,15 @@ namespace MWGui void LoadingScreen::findSplashScreens() { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string pattern = "Splash/"; - mResourceSystem->getVFS()->normalizeFilename(pattern); + auto isSupportedExtension = [](const std::string_view& ext) { + static const std::array supported_extensions{ {"tga", "dds", "ktx", "png", "bmp", "jpeg", "jpg"} }; + return !ext.empty() && std::find(supported_extensions.begin(), supported_extensions.end(), ext) != supported_extensions.end(); + }; - /* priority given to the left */ - const std::array supported_extensions {{".tga", ".dds", ".ktx", ".png", ".bmp", ".jpeg", ".jpg"}}; - - auto found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator("Splash/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos) - { - for(auto const& extension: supported_extensions) - { - if (name.compare(pos, name.size() - pos, extension) == 0) - { - mSplashScreens.push_back(found->first); - break; /* based on priority */ - } - } - } - } - else - break; - ++found; + if (isSupportedExtension(Misc::getFileExtension(name))) + mSplashScreens.push_back(name); } if (mSplashScreens.empty()) Log(Debug::Warning) << "Warning: no splash screens found!"; diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index 6e8804a43f..388bbc7d48 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -30,10 +30,13 @@ #include "confirmationdialog.hpp" #include "tooltips.hpp" +#include + namespace { const int cellSize = Constants::CellSizeInUnits; + constexpr float speed = 1.08f; //the zoom speed, it should be greater than 1 enum LocalMapWidgetDepth { @@ -84,6 +87,22 @@ namespace setColour(mHoverColour); } }; + + MyGUI::IntRect createRect(const MyGUI::IntPoint& center, int radius) + { + return { center.left - radius, center.top + radius, center.left + radius, center.top - radius }; + } + + int getLocalViewingDistance() + { + if (!Settings::Manager::getBool("allow zooming", "Map")) + return Constants::CellGridRadius; + if (!Settings::Manager::getBool("distant terrain", "Terrain")) + return Constants::CellGridRadius; + const int maxLocalViewingDistance = std::max(Settings::Manager::getInt("max local viewing distance", "Map"), Constants::CellGridRadius); + const int viewingDistanceInCells = Settings::Manager::getFloat("viewing distance", "Camera") / Constants::CellSizeInUnits; + return std::min(maxLocalViewingDistance, viewingDistanceInCells); + } } namespace MWGui @@ -167,7 +186,7 @@ namespace MWGui , mFogOfWarToggled(true) , mFogOfWarEnabled(fogOfWarEnabled) , mMapWidgetSize(0) - , mNumCells(0) + , mNumCells(1) , mCellDistance(0) , mCustomMarkers(markers) , mMarkerUpdateTimer(0.0f) @@ -183,12 +202,12 @@ namespace MWGui mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } - void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass) + void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance) { mLocalMap = widget; mCompass = compass; mMapWidgetSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); - mCellDistance = Constants::CellGridRadius; + mCellDistance = cellDistance; mNumCells = mCellDistance * 2 + 1; mLocalMap->setCanvasSize(mMapWidgetSize*mNumCells, mMapWidgetSize*mNumCells); @@ -234,65 +253,94 @@ namespace MWGui void LocalMapBase::applyFogOfWar() { - for (int mx=0; mxsetImageTexture(""); - entry.mFogTexture.reset(); - continue; - } + entry.mFogWidget->setImageTexture(""); + entry.mFogTexture.reset(); } } redraw(); } - MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) + MyGUI::IntPoint LocalMapBase::getPosition(int cellX, int cellY, float nX, float nY) const { - MyGUI::IntPoint widgetPos; + // normalized cell coordinates + auto mapWidgetSize = getWidgetSize(); + return MyGUI::IntPoint( + std::round(nX * mapWidgetSize + (mCellDistance + (cellX - mCurX)) * mapWidgetSize), + std::round(nY * mapWidgetSize + (mCellDistance - (cellY - mCurY)) * mapWidgetSize) + ); + } + + MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const + { + int cellX, cellY; // normalized cell coordinates float nX,nY; if (!mInterior) { - int cellX, cellY; MWBase::Environment::get().getWorld()->positionToIndex(worldX, worldY, cellX, cellY); nX = (worldX - cellSize * cellX) / cellSize; // Image space is -Y up, cells are Y up nY = 1 - (worldY - cellSize * cellY) / cellSize; - - float cellDx = static_cast(cellX - mCurX); - float cellDy = static_cast(cellY - mCurY); - - markerPos.cellX = cellX; - markerPos.cellY = cellY; - - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + cellDx) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - cellDy) * mMapWidgetSize)); } else - { - int cellX, cellY; - osg::Vec2f worldPos (worldX, worldY); - mLocalMapRender->worldToInteriorMapPosition(worldPos, nX, nY, cellX, cellY); - - markerPos.cellX = cellX; - markerPos.cellY = cellY; - - // Image space is -Y up, cells are Y up - widgetPos = MyGUI::IntPoint(static_cast(nX * mMapWidgetSize + (mCellDistance + (cellX - mCurX)) * mMapWidgetSize), - static_cast(nY * mMapWidgetSize + (mCellDistance - (cellY - mCurY)) * mMapWidgetSize)); - } + mLocalMapRender->worldToInteriorMapPosition({ worldX, worldY }, nX, nY, cellX, cellY); + markerPos.cellX = cellX; + markerPos.cellY = cellY; markerPos.nX = nX; markerPos.nY = nY; - return widgetPos; + return getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + } + + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates(float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const + { + int halfMarkerSize = markerSize / 2; + auto position = getMarkerPosition(worldX, worldY, markerPos); + return MyGUI::IntCoord(position.left - halfMarkerSize, position.top - halfMarkerSize, markerSize, markerSize); + } + + MyGUI::Widget* LocalMapBase::createDoorMarker(const std::string& name, const MyGUI::VectorString& notes, float x, float y) const + { + MarkerUserData data(mLocalMapRender); + data.notes = notes; + data.caption = name; + MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", + getMarkerCoordinates(x, y, data, 8), MyGUI::Align::Default); + markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); + markerWidget->setDepth(Local_MarkerLayer); + markerWidget->setNeedMouseFocus(true); + // Used by tooltips to not show the tooltip if marker is hidden by fog of war + markerWidget->setUserString("ToolTipType", "MapMarker"); + + markerWidget->setUserData(data); + return markerWidget; + } + + void LocalMapBase::centerView() + { + MyGUI::IntPoint pos = mCompass->getPosition() + MyGUI::IntPoint{ 16, 16 }; + MyGUI::IntSize viewsize = mLocalMap->getSize(); + MyGUI::IntPoint viewOffset((viewsize.width / 2) - pos.left, (viewsize.height / 2) - pos.top); + mLocalMap->setViewOffset(viewOffset); + } + + MyGUI::IntCoord LocalMapBase::getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const + { + MarkerUserData& markerPos(*widget->getUserData()); + auto position = getPosition(markerPos.cellX, markerPos.cellY, markerPos.nX, markerPos.nY); + return MyGUI::IntCoord(position.left - markerSize / 2, position.top - markerSize / 2, markerSize, markerSize); + } + + std::vector& LocalMapBase::currentDoorMarkersWidgets() + { + return mInterior ? mInteriorDoorMarkerWidgets : mExteriorDoorMarkerWidgets; } void LocalMapBase::updateCustomMarkers() @@ -317,13 +365,8 @@ namespace MWGui const ESM::CustomMarker& marker = it->second; MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.mWorldX, marker.mWorldY, markerPos); - - MyGUI::IntCoord widgetCoord(widgetPos.left - 8, - widgetPos.top - 8, - 16, 16); MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); @@ -346,6 +389,38 @@ namespace MWGui if (x==mCurX && y==mCurY && mInterior==interior && !mChanged) return; // don't do anything if we're still in the same cell + if (!interior && !(x == mCurX && y == mCurY)) + { + const MyGUI::IntRect intersection = { + std::max(x, mCurX) - mCellDistance, std::max(y, mCurY) - mCellDistance, + std::min(x, mCurX) + mCellDistance, std::min(y, mCurY) + mCellDistance + }; + + const MyGUI::IntRect activeGrid = createRect({ x, y }, Constants::CellGridRadius); + const MyGUI::IntRect currentView = createRect({ x, y }, mCellDistance); + + mExteriorDoorMarkerWidgets.clear(); + for (auto& [coord, doors] : mExteriorDoorsByCell) + { + if (!mHasALastActiveCell || !currentView.inside({ coord.first, coord.second }) || activeGrid.inside({ coord.first, coord.second })) + { + mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), doors.begin(), doors.end()); + doors.clear(); + } + else + mExteriorDoorMarkerWidgets.insert(mExteriorDoorMarkerWidgets.end(), doors.begin(), doors.end()); + } + + for (auto& widget : mDoorMarkersToRecycle) + widget->setVisible(false); + + for (auto const& cell : mMaps) + { + if (mHasALastActiveCell && !intersection.inside({ cell.mCellX, cell.mCellY })) + mLocalMapRender->removeExteriorCell(cell.mCellX, cell.mCellY); + } + } + mCurX = x; mCurY = y; mInterior = interior; @@ -370,6 +445,12 @@ namespace MWGui // If we don't do this, door markers that should be disabled will still appear on the map. mNeedDoorMarkersUpdate = true; + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + if (!mInterior) + mHasALastActiveCell = true; + updateMagicMarkers(); updateCustomMarkers(); } @@ -385,21 +466,26 @@ namespace MWGui mLocalMap->getParent()->_updateChilds(); } + float LocalMapBase::getWidgetSize() const + { + return mLocalMapZoom * mMapWidgetSize; + } + void LocalMapBase::setPlayerPos(int cellX, int cellY, const float nx, const float ny) { - MyGUI::IntPoint pos(static_cast(mMapWidgetSize * mCellDistance + nx*mMapWidgetSize - 16), static_cast(mMapWidgetSize * mCellDistance + ny*mMapWidgetSize - 16)); - pos.left += (cellX - mCurX) * mMapWidgetSize; - pos.top -= (cellY - mCurY) * mMapWidgetSize; + MyGUI::IntPoint pos = getPosition(cellX, cellY, nx, ny) - MyGUI::IntPoint{ 16, 16 }; if (pos != mCompass->getPosition()) { notifyPlayerUpdate (); mCompass->setPosition(pos); - MyGUI::IntPoint middle (pos.left+16, pos.top+16); - MyGUI::IntCoord viewsize = mLocalMap->getCoord(); - MyGUI::IntPoint viewOffset((viewsize.width / 2) - middle.left, (viewsize.height / 2) - middle.top); - mLocalMap->setViewOffset(viewOffset); + } + osg::Vec2f curPos((cellX + nx) * cellSize, (cellY + 1 - ny) * cellSize); + if ((curPos - mCurPos).length2() > 0.001) + { + mCurPos = curPos; + centerView(); } } @@ -449,17 +535,14 @@ namespace MWGui { const ESM::Position& worldPos = ptr.getRefData().getPosition(); MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(worldPos.pos[0], worldPos.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); ++counter; MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(worldPos.pos[0], worldPos.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); markerWidget->setImageCoord(MyGUI::IntCoord(0,0,8,8)); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } } @@ -545,34 +628,32 @@ namespace MWGui void LocalMapBase::updateDoorMarkers() { - // clear all previous door markers - for (MyGUI::Widget* widget : mDoorMarkerWidgets) - MyGUI::Gui::getInstance().destroyWidget(widget); - mDoorMarkerWidgets.clear(); - + std::vector doors; MWBase::World* world = MWBase::Environment::get().getWorld(); - // Retrieve the door markers we want to show - std::vector doors; + mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), mInteriorDoorMarkerWidgets.begin(), mInteriorDoorMarkerWidgets.end()); + mInteriorDoorMarkerWidgets.clear(); + if (mInterior) { + for (MyGUI::Widget* widget : mExteriorDoorMarkerWidgets) + widget->setVisible(false); + MWWorld::CellStore* cell = world->getInterior (mPrefix); world->getDoorMarkers(cell, doors); } else { - for (int dX=-mCellDistance; dX<=mCellDistance; ++dX) + for (MapEntry& entry : mMaps) { - for (int dY=-mCellDistance; dY<=mCellDistance; ++dY) - { - MWWorld::CellStore* cell = world->getExterior (mCurX+dX, mCurY+dY); - world->getDoorMarkers(cell, doors); - } + if (!entry.mMapTexture && !widgetCropped(entry.mMapWidget, mLocalMap)) + world->getDoorMarkers(world->getExterior(entry.mCellX, entry.mCellY), doors); } + if (doors.empty()) + return; } // Create a widget for each marker - int counter = 0; for (MWBase::World::DoorMarker& marker : doors) { std::vector destNotes; @@ -580,28 +661,33 @@ namespace MWGui for (CustomMarkerCollection::ContainerType::const_iterator iter = markers.first; iter != markers.second; ++iter) destNotes.push_back(iter->second.mNote); - MarkerUserData data (mLocalMapRender); - data.notes = destNotes; - data.caption = marker.name; - MyGUI::IntPoint widgetPos = getMarkerPosition(marker.x, marker.y, data); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); - ++counter; - MarkerWidget* markerWidget = mLocalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); - markerWidget->setNormalColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setHoverColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal_over}"))); - markerWidget->setDepth(Local_MarkerLayer); - markerWidget->setNeedMouseFocus(true); - // Used by tooltips to not show the tooltip if marker is hidden by fog of war - markerWidget->setUserString("ToolTipType", "MapMarker"); + MyGUI::Widget* markerWidget = nullptr; + MarkerUserData* data; + if (mDoorMarkersToRecycle.empty()) + { + markerWidget = createDoorMarker(marker.name, destNotes, marker.x, marker.y); + data = markerWidget->getUserData(); + doorMarkerCreated(markerWidget); + } + else + { + markerWidget = (MarkerWidget*)mDoorMarkersToRecycle.back(); + mDoorMarkersToRecycle.pop_back(); - markerWidget->setUserData(data); - doorMarkerCreated(markerWidget); + data = markerWidget->getUserData(); + data->notes = destNotes; + data->caption = marker.name; + markerWidget->setCoord(getMarkerCoordinates(marker.x, marker.y, *data, 8)); + markerWidget->setVisible(true); + } - mDoorMarkerWidgets.push_back(markerWidget); + currentDoorMarkersWidgets().push_back(markerWidget); + if (!mInterior) + mExteriorDoorsByCell[{data->cellX, data->cellY}].push_back(markerWidget); } + + for (auto& widget : mDoorMarkersToRecycle) + widget->setVisible(false); } void LocalMapBase::updateMagicMarkers() @@ -623,23 +709,46 @@ namespace MWGui && (!mInterior || Misc::StringUtils::ciEqual(markedCell->getCell()->mName, mPrefix))) { MarkerUserData markerPos (mLocalMapRender); - MyGUI::IntPoint widgetPos = getMarkerPosition(markedPosition.pos[0], markedPosition.pos[1], markerPos); - MyGUI::IntCoord widgetCoord(widgetPos.left - 4, - widgetPos.top - 4, - 8, 8); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", - widgetCoord, MyGUI::Align::Default); + getMarkerCoordinates(markedPosition.pos[0], markedPosition.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture("textures\\menu_map_smark.dds"); markerWidget->setNeedMouseFocus(false); + markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } redraw(); } - // ------------------------------------------------------------------------------------------ + void LocalMapBase::updateLocalMap() + { + auto mapWidgetSize = getWidgetSize(); + mLocalMap->setCanvasSize(mapWidgetSize * mNumCells, mapWidgetSize * mNumCells); + const auto size = MyGUI::IntSize(std::ceil(mapWidgetSize), std::ceil(mapWidgetSize)); + for (auto& entry : mMaps) + { + const auto position = getPosition(entry.mCellX, entry.mCellY, 0, 0); + entry.mMapWidget->setCoord({ position, size }); + entry.mFogWidget->setCoord({ position, size }); + } + + MarkerUserData markerPos(mLocalMapRender); + for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) + widget->setCoord(getMarkerCoordinates(widget, 8)); + + for (MyGUI::Widget* widget : mCustomMarkerWidgets) + { + const auto& marker = *widget->getUserData(); + widget->setCoord(getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16)); + } + + for (MyGUI::Widget* widget : mMagicMarkerWidgets) + widget->setCoord(getMarkerCoordinates(widget, 8)); + } + + // ------------------------------------------------------------------------------------------ MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) : WindowPinnableBase("openmw_map_window.layout") , LocalMapBase(customMarkers, localMapRender) @@ -690,14 +799,19 @@ namespace MWGui getWidget(mEventBoxGlobal, "EventBoxGlobal"); mEventBoxGlobal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxGlobal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + const bool allowZooming = Settings::Manager::getBool("allow zooming", "Map"); + if(allowZooming) + mEventBoxGlobal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); mEventBoxGlobal->setDepth(Global_ExploreOverlayLayer); getWidget(mEventBoxLocal, "EventBoxLocal"); mEventBoxLocal->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); + if (allowZooming) + mEventBoxLocal->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, getLocalViewingDistance()); mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); @@ -745,10 +859,11 @@ namespace MWGui MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); - int x = int(widgetPos.left/float(mMapWidgetSize))-mCellDistance; - int y = (int(widgetPos.top/float(mMapWidgetSize))-mCellDistance)*-1; - float nX = widgetPos.left/float(mMapWidgetSize) - int(widgetPos.left/float(mMapWidgetSize)); - float nY = widgetPos.top/float(mMapWidgetSize) - int(widgetPos.top/float(mMapWidgetSize)); + auto mapWidgetSize = getWidgetSize(); + int x = int(widgetPos.left/float(mapWidgetSize))-mCellDistance; + int y = (int(widgetPos.top/float(mapWidgetSize))-mCellDistance)*-1; + float nX = widgetPos.left/float(mapWidgetSize) - int(widgetPos.left/float(mapWidgetSize)); + float nY = widgetPos.top/float(mapWidgetSize) - int(widgetPos.top/float(mapWidgetSize)); x += mCurX; y += mCurY; @@ -781,6 +896,110 @@ namespace MWGui mEditNoteDialog.setText(""); } + void MapWindow::onMapZoomed(MyGUI::Widget* sender, int rel) + { + const static int localWidgetSize = Settings::Manager::getInt("local map widget size", "Map"); + const static int globalCellSize = Settings::Manager::getInt("global map cell size", "Map"); + + const bool zoomOut = rel < 0; + const bool zoomIn = !zoomOut; + const double speedDiff = zoomOut ? 1.0 / speed : speed; + const float localMapSizeInUnits = localWidgetSize * mNumCells; + + const float currentMinLocalMapZoom = std::max({ + (float(globalCellSize) * 4.f) / float(localWidgetSize), + float(mLocalMap->getWidth()) / localMapSizeInUnits, + float(mLocalMap->getHeight()) / localMapSizeInUnits + }); + + if (mGlobal) + { + const float currentGlobalZoom = mGlobalMapZoom; + const float currentMinGlobalMapZoom = std::min( + float(mGlobalMap->getWidth()) / float(mGlobalMapRender->getWidth()), + float(mGlobalMap->getHeight()) / float(mGlobalMapRender->getHeight()) + ); + + mGlobalMapZoom *= speedDiff; + + if (zoomIn && mGlobalMapZoom > 4.f) + { + mGlobalMapZoom = currentGlobalZoom; + mLocalMapZoom = currentMinLocalMapZoom; + onWorldButtonClicked(nullptr); + updateLocalMap(); + return; //the zoom in is too big + } + + if (zoomOut && mGlobalMapZoom < currentMinGlobalMapZoom) + { + mGlobalMapZoom = currentGlobalZoom; + return; //the zoom out is too big, we have reach the borders of the widget + } + } + else + { + auto const currentLocalZoom = mLocalMapZoom; + mLocalMapZoom *= speedDiff; + + if (zoomIn && mLocalMapZoom > 4.0f) + { + mLocalMapZoom = currentLocalZoom; + return; //the zoom in is too big + } + + if (zoomOut && mLocalMapZoom < currentMinLocalMapZoom) + { + mLocalMapZoom = currentLocalZoom; + + float zoomRatio = 4.f/ mGlobalMapZoom; + mGlobalMapZoom = 4.f; + onWorldButtonClicked(nullptr); + + zoomOnCursor(zoomRatio); + return; //the zoom out is too big, we switch to the global map + } + + if (zoomOut) + mNeedDoorMarkersUpdate = true; + } + zoomOnCursor(speedDiff); + } + + void MapWindow::zoomOnCursor(float speedDiff) + { + auto map = mGlobal ? mGlobalMap : mLocalMap; + auto cursor = MyGUI::InputManager::getInstance().getMousePosition() - map->getAbsolutePosition(); + auto centerView = map->getViewOffset() - cursor; + + mGlobal? updateGlobalMap() : updateLocalMap(); + + map->setViewOffset(MyGUI::IntPoint( + std::round(centerView.left * speedDiff) + cursor.left, + std::round(centerView.top * speedDiff) + cursor.top + )); + } + + void MapWindow::updateGlobalMap() + { + resizeGlobalMap(); + + float x = mCurPos.x(), y = mCurPos.y(); + if (mInterior) + { + auto pos = MWBase::Environment::get().getWorld()->getPlayer().getLastKnownExteriorPosition(); + x = pos.x(); + y = pos.y(); + } + setGlobalMapPlayerPosition(x, y); + + for (auto& [marker, col] : mGlobalMapMarkers) + { + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), col.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } + } + void MapWindow::onChangeScrollWindowCoord(MyGUI::Widget* sender) { MyGUI::IntCoord currentCoordinates = sender->getCoord(); @@ -804,8 +1023,7 @@ namespace MWGui void MapWindow::renderGlobalMap() { mGlobalMapRender->render(); - mGlobalMap->setCanvasSize (mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); - mGlobalMapImage->setSize(mGlobalMapRender->getWidth(), mGlobalMapRender->getHeight()); + resizeGlobalMap(); } MapWindow::~MapWindow() @@ -818,6 +1036,39 @@ namespace MWGui setTitle("#{sCell=" + cellName + "}"); } + MyGUI::IntCoord MapWindow::createMarkerCoords(float x, float y, float agregatedWeight) const + { + float worldX, worldY; + worldPosToGlobalMapImageSpace((x + 0.5f) * Constants::CellSizeInUnits, (y + 0.5f)* Constants::CellSizeInUnits, worldX, worldY); + + const float markerSize = getMarkerSize(agregatedWeight); + const float halfMarkerSize = markerSize / 2.0f; + return MyGUI::IntCoord( + static_cast(worldX - halfMarkerSize), + static_cast(worldY - halfMarkerSize), + markerSize, markerSize); + } + + MyGUI::Widget* MapWindow::createMarker(const std::string& name, float x, float y, float agregatedWeight) + { + MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", + createMarkerCoords(x, y, agregatedWeight), MyGUI::Align::Default); + markerWidget->setVisible(markerWidget->getHeight() >= 6.0); + markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); + setGlobalMapMarkerTooltip(markerWidget, x, y); + + markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); + + markerWidget->setNeedMouseFocus(true); + markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); + markerWidget->setDepth(Global_MarkerLayer); + markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); + markerWidget->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); + markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + + return markerWidget; + } + void MapWindow::addVisitedLocation(const std::string& name, int x, int y) { CellId cell; @@ -825,31 +1076,33 @@ namespace MWGui cell.second = y; if (mMarkers.insert(cell).second) { - float worldX, worldY; - mGlobalMapRender->cellTopLeftCornerToImageSpace (x, y, worldX, worldY); + MapMarkerType mapMarkerWidget = { osg::Vec2f(x, y), createMarker(name, x, y, 0) }; + mGlobalMapMarkers.emplace(mapMarkerWidget, std::vector()); - int markerSize = 12; - int offset = mGlobalMapRender->getCellSize()/2 - markerSize/2; - MyGUI::IntCoord widgetCoord( - static_cast(worldX * mGlobalMapRender->getWidth()+offset), - static_cast(worldY * mGlobalMapRender->getHeight() + offset), - markerSize, markerSize); + std::string name_ = name.substr(0, name.find(',')); + auto& entry = mGlobalMapMarkersByName[name_]; + if (!entry.widget) + { + entry = { osg::Vec2f(x, y), entry.widget }; //update the coords - MyGUI::Widget* markerWidget = mGlobalMap->createWidget("MarkerButton", - widgetCoord, MyGUI::Align::Default); + entry.widget = createMarker(name_, entry.position.x(), entry.position.y(), 1); + mGlobalMapMarkers.emplace(entry, std::vector{ entry }); + } + else + { + auto it = mGlobalMapMarkers.find(entry); + auto& marker = const_cast(it->first); + auto& elements = it->second; + elements.emplace_back(mapMarkerWidget); - markerWidget->setUserString("Caption_TextOneLine", "#{sCell=" + name + "}"); + //we compute the barycenter of the entry elements => it will be the place on the world map for the agregated widget + marker.position = std::accumulate(elements.begin(), elements.end(), osg::Vec2f(0.f, 0.f), [](const auto& left, const auto& right) { + return left + right.position; + }) / float(elements.size()); - setGlobalMapMarkerTooltip(markerWidget, x, y); - - markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); - - markerWidget->setNeedMouseFocus(true); - markerWidget->setColour(MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=normal}"))); - markerWidget->setDepth(Global_MarkerLayer); - markerWidget->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); - markerWidget->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); - mGlobalMapMarkers[std::make_pair(x,y)] = markerWidget; + marker.widget->setCoord(createMarkerCoords(marker.position.x(), marker.position.y(), elements.size())); + marker.widget->setVisible(marker.widget->getHeight() >= 6); + } } } @@ -891,17 +1144,33 @@ namespace MWGui } } + float MapWindow::getMarkerSize(size_t agregatedWeight) const + { + float markerSize = 12.f * mGlobalMapZoom; + if (mGlobalMapZoom < 1) + return markerSize * std::sqrt(agregatedWeight); //we want to see agregated object + return agregatedWeight ? 0 : markerSize; //we want to see only original markers (i.e. non agregated) + } + + void MapWindow::resizeGlobalMap() + { + mGlobalMap->setCanvasSize(mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + mGlobalMapImage->setSize(mGlobalMapRender->getWidth() * mGlobalMapZoom, mGlobalMapRender->getHeight() * mGlobalMapZoom); + } + + void MapWindow::worldPosToGlobalMapImageSpace(float x, float y, float& imageX, float& imageY) const + { + mGlobalMapRender->worldPosToImageSpace(x, y, imageX, imageY); + imageX *= mGlobalMapZoom; + imageY *= mGlobalMapZoom; + } + void MapWindow::updateCustomMarkers() { LocalMapBase::updateCustomMarkers(); - for (auto& widgetPair : mGlobalMapMarkers) - { - int x = widgetPair.first.first; - int y = widgetPair.first.second; - MyGUI::Widget* markerWidget = widgetPair.second; - setGlobalMapMarkerTooltip(markerWidget, x, y); - } + for (auto& [widgetPair, ignore]: mGlobalMapMarkers) + setGlobalMapMarkerTooltip(widgetPair.widget, widgetPair.position.x(), widgetPair.position.y()); } void MapWindow::onDragStart(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) @@ -917,7 +1186,10 @@ namespace MWGui MyGUI::IntPoint diff = MyGUI::IntPoint(_left, _top) - mLastDragPos; if (!mGlobal) + { + mNeedDoorMarkersUpdate = true; mLocalMap->setViewOffset( mLocalMap->getViewOffset() + diff ); + } else mGlobalMap->setViewOffset( mGlobalMap->getViewOffset() + diff ); @@ -934,9 +1206,6 @@ namespace MWGui mButton->setCaptionWithReplacing( mGlobal ? "#{sLocal}" : "#{sWorld}"); - - if (mGlobal) - globalMapUpdatePlayer (); } void MapWindow::onPinToggled() @@ -978,19 +1247,21 @@ namespace MWGui setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } + void MapWindow::centerView() + { + LocalMapBase::centerView(); + // set the view offset so that player is in the center + MyGUI::IntSize viewsize = mGlobalMap->getSize(); + MyGUI::IntPoint pos = mPlayerArrowGlobal->getPosition() + MyGUI::IntPoint{ 16,16 }; + MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - pos.left), static_cast(viewsize.height * 0.5f - pos.top)); + mGlobalMap->setViewOffset(viewoffs); + } + void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) { float x, y; - mGlobalMapRender->worldPosToImageSpace (worldX, worldY, x, y); - x *= mGlobalMapRender->getWidth(); - y *= mGlobalMapRender->getHeight(); - + worldPosToGlobalMapImageSpace(worldX, worldY, x, y); mPlayerArrowGlobal->setPosition(MyGUI::IntPoint(static_cast(x - 16), static_cast(y - 16))); - - // set the view offset so that player is in the center - MyGUI::IntSize viewsize = mGlobalMap->getSize(); - MyGUI::IntPoint viewoffs(static_cast(viewsize.width * 0.5f - x), static_cast(viewsize.height *0.5 - y)); - mGlobalMap->setViewOffset(viewoffs); } void MapWindow::setGlobalMapPlayerDir(const float x, const float y) @@ -1027,8 +1298,9 @@ namespace MWGui mChanged = true; for (auto& widgetPair : mGlobalMapMarkers) - MyGUI::Gui::getInstance().destroyWidget(widgetPair.second); + MyGUI::Gui::getInstance().destroyWidget(widgetPair.first.widget); mGlobalMapMarkers.clear(); + mGlobalMapMarkersByName.clear(); } void MapWindow::write(ESM::ESMWriter &writer, Loading::Listener& progress) @@ -1075,12 +1347,14 @@ namespace MWGui marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); marker->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onCustomMarkerDoubleClicked); + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); } void MapWindow::doorMarkerCreated(MyGUI::Widget *marker) { marker->eventMouseDrag += MyGUI::newDelegate(this, &MapWindow::onMouseDrag); marker->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); + marker->eventMouseWheel += MyGUI::newDelegate(this, &MapWindow::onMapZoomed); } // ------------------------------------------------------------------- diff --git a/apps/openmw/mwgui/mapwindow.hpp b/apps/openmw/mwgui/mapwindow.hpp index 7e8092f289..d3cd626475 100644 --- a/apps/openmw/mwgui/mapwindow.hpp +++ b/apps/openmw/mwgui/mapwindow.hpp @@ -9,6 +9,7 @@ #include #include +#include namespace MWRender { @@ -72,7 +73,7 @@ namespace MWGui public: LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled = true); virtual ~LocalMapBase(); - void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass); + void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance = Constants::CellGridRadius); void setCellPrefix(const std::string& prefix); void setActiveCell(const int x, const int y, bool interior=false); @@ -107,9 +108,15 @@ namespace MWGui }; protected: + void updateLocalMap(); + + float mLocalMapZoom = 1.f; MWRender::LocalMap* mLocalMapRender; - int mCurX, mCurY; + int mCurX, mCurY; //the position of the active cell on the global map (in cell coords) + bool mHasALastActiveCell = false; + osg::Vec2f mCurPos; //the position of the player in the world (in cell coords) + bool mInterior; MyGUI::ScrollView* mLocalMap; MyGUI::ImageBox* mCompass; @@ -141,17 +148,27 @@ namespace MWGui std::vector mMaps; // Keep track of created marker widgets, just to easily remove them later. - std::vector mDoorMarkerWidgets; + std::vector mExteriorDoorMarkerWidgets; + std::map, std::vector> mExteriorDoorsByCell; + std::vector mInteriorDoorMarkerWidgets; std::vector mMagicMarkerWidgets; std::vector mCustomMarkerWidgets; + std::vector mDoorMarkersToRecycle; + + std::vector& currentDoorMarkersWidgets(); virtual void updateCustomMarkers(); void applyFogOfWar(); - MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos); + MyGUI::IntPoint getPosition(int cellX, int cellY, float nx, float ny) const; + MyGUI::IntPoint getMarkerPosition (float worldX, float worldY, MarkerUserData& markerPos) const; + MyGUI::IntCoord getMarkerCoordinates(float worldX, float worldY, MarkerUserData& markerPos, size_t markerSize) const; + MyGUI::Widget* createDoorMarker(const std::string& name, const MyGUI::VectorString& notes, float x, float y) const; + MyGUI::IntCoord getMarkerCoordinates(MyGUI::Widget* widget, size_t markerSize) const; virtual void notifyPlayerUpdate() {} + virtual void centerView(); virtual void notifyMapChanged() {} virtual void customMarkerCreated(MyGUI::Widget* marker) {} @@ -163,15 +180,17 @@ namespace MWGui void addDetectionMarkers(int type); void redraw(); + float getWidgetSize() const; float mMarkerUpdateTimer; float mLastDirectionX; float mLastDirectionY; + bool mNeedDoorMarkersUpdate; + private: void updateDoorMarkers(); - bool mNeedDoorMarkersUpdate; }; class EditNoteDialog : public MWGui::WindowModal @@ -244,6 +263,9 @@ namespace MWGui void onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onWorldButtonClicked(MyGUI::Widget* _sender); void onMapDoubleClicked(MyGUI::Widget* sender); + void onMapZoomed(MyGUI::Widget* sender, int rel); + void zoomOnCursor(float speedDiff); + void updateGlobalMap(); void onCustomMarkerDoubleClicked(MyGUI::Widget* sender); void onNoteEditOk(); void onNoteEditDelete(); @@ -252,6 +274,12 @@ namespace MWGui void onChangeScrollWindowCoord(MyGUI::Widget* sender); void globalMapUpdatePlayer(); void setGlobalMapMarkerTooltip(MyGUI::Widget* widget, int x, int y); + float getMarkerSize(size_t agregatedWeight) const; + void resizeGlobalMap(); + void worldPosToGlobalMapImageSpace(float x, float z, float& imageX, float& imageY) const; + MyGUI::IntCoord createMarkerCoords(float x, float y, float agregatedWeight) const; + MyGUI::Widget* createMarker(const std::string& name, float x, float y, float agregatedWeight); + MyGUI::ScrollView* mGlobalMap; std::unique_ptr mGlobalMapTexture; @@ -273,9 +301,21 @@ namespace MWGui MyGUI::Button* mEventBoxGlobal; MyGUI::Button* mEventBoxLocal; + float mGlobalMapZoom = 1.0f; MWRender::GlobalMap* mGlobalMapRender; - std::map, MyGUI::Widget*> mGlobalMapMarkers; + struct MapMarkerType + { + osg::Vec2f position; + MyGUI::Widget* widget = nullptr; + + bool operator<(const MapMarkerType& right) const { + return widget < right.widget; + } + }; + + std::map mGlobalMapMarkersByName; + std::map> mGlobalMapMarkers; EditNoteDialog mEditNoteDialog; ESM::CustomMarker mEditingMarker; @@ -288,6 +328,7 @@ namespace MWGui void notifyPlayerUpdate() override; + void centerView() override; }; } #endif diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 4907a247ff..ed6633c983 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -376,7 +376,9 @@ namespace MWGui { for (const std::string& keyword : keywords) { - if(Misc::StringUtils::ciEqual(MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}"), button->getCaption())) + if (Misc::StringUtils::ciEqual( + MyGUI::LanguageManager::getInstance().replaceTags("#{" + keyword + "}").asUTF8(), + button->getCaption().asUTF8())) { return button; } diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index c4d608443c..086135d035 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -450,6 +450,7 @@ namespace MWGui osg::ref_ptr texture (new osg::Texture2D); texture->setImage(result.getImage()); + texture->setInternalFormat(GL_RGB); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index eb51f560be..49870a9ddf 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -99,10 +99,8 @@ namespace MWGui std::vector spellsToSort; - for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter) + for (const ESM::Spell* spell : merchantSpells) { - const ESM::Spell* spell = iter->first; - if (spell->mData.mType!=ESM::Spell::ST_Spell) continue; // don't try to sell diseases, curses or powers @@ -115,10 +113,10 @@ namespace MWGui continue; } - if (playerHasSpell(iter->first->mId)) + if (playerHasSpell(spell->mId)) continue; - spellsToSort.push_back(iter->first); + spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 01653d9e6f..8c0ea865a8 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -520,10 +520,8 @@ namespace MWGui std::vector knownEffects; - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; - // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 405abfbae7..0673446fe7 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -24,50 +24,33 @@ namespace MWGui { - - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime, float totalTime) - { - MagicEffectInfo newEffectSource; - newEffectSource.mKey = key; - newEffectSource.mMagnitude = static_cast(magnitude); - newEffectSource.mPermanent = mIsPermanent; - newEffectSource.mRemainingTime = remainingTime; - newEffectSource.mSource = sourceName; - newEffectSource.mTotalTime = totalTime; - - mEffectSources[key.mId].push_back(newEffectSource); - } - - void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize) { - // TODO: Tracking add/remove/expire would be better than force updating every frame - MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); - - EffectSourceVisitor visitor; - - // permanent item enchantments & permanent spells - visitor.mIsPermanent = true; - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - store.visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - - // now add lasting effects - visitor.mIsPermanent = false; - stats.getActiveSpells().visitEffectSources(visitor); - - std::map >& effects = visitor.mEffectSources; + std::map> effects; + for(const auto& params : stats.getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + MagicEffectInfo newEffectSource; + newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg); + newEffectSource.mMagnitude = static_cast(effect.mMagnitude); + newEffectSource.mPermanent = effect.mDuration == -1.f; + newEffectSource.mRemainingTime = effect.mTimeLeft; + newEffectSource.mSource = params.getDisplayName(); + newEffectSource.mTotalTime = effect.mDuration; + effects[effect.mEffectId].push_back(newEffectSource); + } + } int w=2; - for (auto& effectInfoPair : effects) + for (const auto& [effectId, effectInfos] : effects) { - const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); @@ -78,7 +61,6 @@ namespace MWGui static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get().find("fMagicStartIconBlink")->mValue.getFloat(); - std::vector& effectInfos = effectInfoPair.second; bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index b6aa49e69e..9825162a33 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -37,20 +37,6 @@ namespace MWGui bool mPermanent; // the effect is permanent }; - class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor - { - public: - bool mIsPermanent; - - std::map > mEffectSources; - - virtual ~EffectSourceVisitor() {} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - }; - class SpellIcons { public: diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index fe18b0f082..61ea9ce93a 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -92,9 +92,8 @@ namespace MWGui std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter); - for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index d236305f37..4b8279065c 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -542,4 +542,10 @@ namespace MWGui return; resetReference(); } + + void TradeWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + if(mTradeModel && mTradeModel->usesContainer(ptr)) + MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); + } } diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index 523cb84098..e39d1ebd8e 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -42,6 +42,8 @@ namespace MWGui void resetReference() override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + typedef MyGUI::delegates::CMultiDelegate0 EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index ed7a74b95f..22e3a55566 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -44,7 +44,7 @@ namespace MWGui mSelect->getHeight()); } - void TravelWindow::addDestination(const std::string& name, ESM::Position pos, bool interior) + void TravelWindow::addDestination(const std::string& name, const ESM::Position &pos, bool interior) { int price; diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 00b7db7305..dd970ee10e 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -31,7 +31,7 @@ namespace MWGui void onCancelButtonClicked(MyGUI::Widget* _sender); void onTravelButtonClick(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); - void addDestination(const std::string& name, ESM::Position pos, bool interior); + void addDestination(const std::string& name, const ESM::Position& pos, bool interior); void clearDestinations(); int mCurrentY; diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 90ef2118de..395bb8414f 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -47,6 +47,8 @@ namespace MWGui /// Called when GUI viewport changes size virtual void onResChange(int width, int height) {} + virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) {} + protected: virtual void onTitleDoubleClicked(); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 544a0927e7..6eeb2d3654 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -35,6 +35,7 @@ #include #include +#include #include @@ -124,7 +125,7 @@ namespace MWGui WindowManager::WindowManager( SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& logpath, const std::string& resourcePath, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath) + ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& userDataPath, bool useShaders) : mOldUpdateMask(0) , mOldCullMask(0) , mStore(nullptr) @@ -275,6 +276,9 @@ namespace MWGui mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); + if (useShaders) + mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); + mStatsWatcher.reset(new StatsWatcher()); } @@ -2220,4 +2224,10 @@ namespace MWGui messageBox(v.mMessage, v.mShowInDialogueMode); scheduledMessageBoxes->clear(); } + + void WindowManager::onDeleteCustomData(const MWWorld::Ptr& ptr) + { + for(auto* window : mWindows) + window->onDeleteCustomData(ptr); + } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 8c7e365ec7..9ec79e0c82 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -135,7 +135,7 @@ namespace MWGui WindowManager(SDL_Window* window, osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& logpath, const std::string& cacheDir, bool consoleOnlyScripts, Translation::Storage& translationDataStorage, - ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& localPath); + ToUTF8::FromType encoding, bool exportFonts, const std::string& versionDescription, const std::string& localPath, bool useShaders); virtual ~WindowManager(); /// Set the ESMStore to use for retrieving of GUI-related strings. @@ -388,6 +388,8 @@ namespace MWGui const std::string& getVersionDescription() const override; + void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + private: unsigned int mOldUpdateMask; unsigned int mOldCullMask; diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index e0fcc5ccfc..e080437d92 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -9,6 +9,7 @@ #include "../mwbase/inputmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -195,6 +196,7 @@ namespace MWInput void ActionManager::executeAction(int action) { + MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::Action, action}); auto* inputManager = MWBase::Environment::get().getInputManager(); auto* windowManager = MWBase::Environment::get().getWindowManager(); // trigger action activated @@ -490,16 +492,17 @@ namespace MWInput if (MyGUI::InputManager::getInstance ().isModalAny()) return; - if (MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Journal - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_MainMenu - && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_Settings - && MWBase::Environment::get().getWindowManager ()->getJournalAllowed()) + MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager(); + if (windowManager->getMode() != MWGui::GM_Journal + && windowManager->getMode() != MWGui::GM_MainMenu + && windowManager->getMode() != MWGui::GM_Settings + && windowManager->getJournalAllowed()) { - MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); + windowManager->pushGuiMode(MWGui::GM_Journal); } - else if (MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Journal)) + else if (windowManager->containsMode(MWGui::GM_Journal)) { - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Journal); + windowManager->removeGuiMode(MWGui::GM_Journal); } } diff --git a/apps/openmw/mwinput/actionmanager.hpp b/apps/openmw/mwinput/actionmanager.hpp index eceac2e94f..2180e1944e 100644 --- a/apps/openmw/mwinput/actionmanager.hpp +++ b/apps/openmw/mwinput/actionmanager.hpp @@ -48,6 +48,7 @@ namespace MWInput void showQuickKeysMenu(); void resetIdleTime(); + float getIdleTime() const { return mTimeIdle; } bool isAlwaysRunActive() const { return mAlwaysRunActive; }; bool isSneaking() const { return mSneaking; }; diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 851e33a87c..ca7911ecc2 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -653,6 +653,16 @@ namespace MWInput return mInputBinder->getKeyBinding(mInputBinder->getControl(actionId), ICS::Control::INCREASE); } + float BindingsManager::getControllerAxisValue(SDL_GameControllerAxis axis) 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); + } + void BindingsManager::actionValueChanged(int action, float currentValue, float previousValue) { MWBase::Environment::get().getInputManager()->resetIdleTime(); diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index 74416d3c7f..5c653f0b3e 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -42,7 +42,8 @@ namespace MWInput bool isLeftOrRightButton(int action, bool joystick) const; bool actionIsActive(int id) const; - float getActionValue(int id) const; + float getActionValue(int id) const; // returns value in range [0, 1] + float getControllerAxisValue(SDL_GameControllerAxis axis) const; // returns value in range [-1, 1] 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 03d492c9cf..fa10ce03cd 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -8,6 +8,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -98,8 +99,8 @@ namespace MWInput // We keep track of our own mouse position, so that moving the mouse while in // game mode does not move the position of the GUI cursor float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); - float xMove = xAxis * dt * 1500.0f / uiScale; - float yMove = yAxis * dt * 1500.0f / uiScale; + float xMove = xAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; + float yMove = yAxis * dt * 1500.0f / uiScale * mGamepadCursorSpeed; float mouseWheelMove = -zAxis * dt * 1500.0f; if (xMove != 0 || yMove != 0 || mouseWheelMove != 0) @@ -198,6 +199,9 @@ namespace MWInput if (!mJoystickEnabled || mBindingsManager->isDetectingBindingState()) return; + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::ControllerPressed, arg.button}); + mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { @@ -240,6 +244,12 @@ namespace MWInput return; } + if (mJoystickEnabled) + { + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::ControllerReleased, arg.button}); + } + if (!mJoystickEnabled || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; diff --git a/apps/openmw/mwinput/controlswitch.cpp b/apps/openmw/mwinput/controlswitch.cpp index 33c4b75dcc..f31744fca6 100644 --- a/apps/openmw/mwinput/controlswitch.cpp +++ b/apps/openmw/mwinput/controlswitch.cpp @@ -57,7 +57,7 @@ namespace MWInput } else if (key == "playerlooking" && !value) { - MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), 0.f, 0.f, 0.f); + MWBase::Environment::get().getWorld()->rotateObject(player.getPlayer(), osg::Vec3f()); } mSwitches[key] = value; } diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 436eab7ad3..31f515afb0 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -150,21 +150,56 @@ namespace MWInput mActionManager->resetIdleTime(); } - std::string InputManager::getActionDescription(int action) + bool InputManager::isIdle() const + { + return mActionManager->getIdleTime() > 0.5; + } + + std::string InputManager::getActionDescription(int action) const { return mBindingsManager->getActionDescription(action); } - std::string InputManager::getActionKeyBindingName(int action) + std::string InputManager::getActionKeyBindingName(int action) const { return mBindingsManager->getActionKeyBindingName(action); } - std::string InputManager::getActionControllerBindingName(int action) + std::string InputManager::getActionControllerBindingName(int action) const { return mBindingsManager->getActionControllerBindingName(action); } + bool InputManager::actionIsActive(int action) const + { + return mBindingsManager->actionIsActive(action); + } + + float InputManager::getActionValue(int action) const + { + return mBindingsManager->getActionValue(action); + } + + float InputManager::getControllerAxisValue(SDL_GameControllerAxis axis) const + { + return mBindingsManager->getControllerAxisValue(axis); + } + + uint32_t InputManager::getMouseButtonsState() const + { + return mMouseManager->getButtonsState(); + } + + int InputManager::getMouseMoveX() const + { + return mMouseManager->getMouseMoveX(); + } + + int InputManager::getMouseMoveY() const + { + return mMouseManager->getMouseMoveY(); + } + std::vector InputManager::getActionKeySorting() { return mBindingsManager->getActionKeySorting(); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index f930836d1c..adb8319498 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -73,9 +73,17 @@ namespace MWInput void toggleControlSwitch (const std::string& sw, bool value) override; bool getControlSwitch (const std::string& sw) override; - std::string getActionDescription (int action) override; - std::string getActionKeyBindingName (int action) override; - std::string getActionControllerBindingName (int action) override; + std::string getActionDescription (int action) const override; + std::string getActionKeyBindingName (int action) const override; + std::string getActionControllerBindingName (int action) const override; + bool actionIsActive(int action) const override; + + float getActionValue(int action) const override; + float getControllerAxisValue(SDL_GameControllerAxis axis) const override; + uint32_t getMouseButtonsState() const override; + int getMouseMoveX() const override; + int getMouseMoveY() const override; + int getNumActions() override { return A_Last; } std::vector getActionKeySorting() override; std::vector getActionControllerSorting() override; @@ -91,6 +99,7 @@ namespace MWInput void readRecord(ESM::ESMReader& reader, uint32_t type) override; void resetIdleTime() override; + bool isIdle() const override; void executeAction(int action) override; diff --git a/apps/openmw/mwinput/keyboardmanager.cpp b/apps/openmw/mwinput/keyboardmanager.cpp index 03db584192..b8019b12ba 100644 --- a/apps/openmw/mwinput/keyboardmanager.cpp +++ b/apps/openmw/mwinput/keyboardmanager.cpp @@ -60,7 +60,10 @@ namespace MWInput mBindingsManager->keyPressed(arg); if (!consumed) - MWBase::Environment::get().getLuaManager()->keyPressed(arg); + { + MWBase::Environment::get().getLuaManager()->inputEvent( + {MWBase::LuaManager::InputEvent::KeyPressed, arg.keysym}); + } input->setJoystickLastUsed(false); } @@ -73,5 +76,6 @@ namespace MWInput if (!mBindingsManager->isDetectingBindingState()) mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); mBindingsManager->keyReleased(arg); + MWBase::Environment::get().getLuaManager()->inputEvent({MWBase::LuaManager::InputEvent::KeyReleased, arg.keysym}); } } diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index cf151dfac7..7810a40ad2 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -34,6 +34,9 @@ namespace MWInput , mMouseWheel(0) , mMouseLookEnabled(false) , mGuiCursorEnabled(true) + , mButtonsState(0) + , mMouseMoveX(0) + , mMouseMoveY(0) { int w,h; SDL_GetWindowSize(window, &w, &h); @@ -196,6 +199,8 @@ namespace MWInput void MouseManager::update(float dt) { + mButtonsState = SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); + if (!mMouseLookEnabled) return; diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 000e7cd0b6..d5504c5f5a 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -38,6 +38,10 @@ 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; } + private: bool mInvertX; bool mInvertY; @@ -53,6 +57,10 @@ namespace MWInput int mMouseWheel; bool mMouseLookEnabled; bool mGuiCursorEnabled; + + uint32_t mButtonsState; + int mMouseMoveX; + int mMouseMoveY; }; } #endif diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index 95a33fed0d..aad70d1a57 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -39,8 +39,8 @@ namespace MWLua } else { - MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos.x(), mPos.y(), mPos.z()); - world->rotateObject(newObj, mRot.x(), mRot.y(), mRot.z()); + MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos); + world->rotateObject(newObj, mRot); } } @@ -51,8 +51,8 @@ namespace MWLua std::array usedSlots; std::fill(usedSlots.begin(), usedSlots.end(), false); - constexpr int anySlot = -1; - auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView, anySlot](int slot, const Item& item) -> bool + static constexpr int anySlot = -1; + auto tryEquipToSlot = [&actor, &store, &usedSlots, &worldView](int slot, const Item& item) -> bool { auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); MWWorld::Ptr itemPtr; @@ -71,7 +71,7 @@ namespace MWLua else { const std::string& recordId = std::get(item); - if (old_it != store.end() && *old_it->getCellRef().getRefIdPtr() == recordId) + if (old_it != store.end() && old_it->getCellRef().getRefIdRef() == recordId) return true; // already equipped itemPtr = store.search(recordId); if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index fee6788b89..9fdda53d9d 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -1,5 +1,7 @@ #include "luabindings.hpp" +#include "luamanagerimp.hpp" + namespace sol { template <> @@ -48,11 +50,16 @@ namespace MWLua asyncId.mContainer->setupUnsavableTimer( TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScript, std::move(callback)); }; + api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) + { + return Callback{std::move(fn), asyncId.mHiddenData}; + }; auto initializer = [](sol::table hiddenData) { LuaUtil::ScriptsContainer::ScriptId id = hiddenData[LuaUtil::ScriptsContainer::ScriptId::KEY]; - return AsyncPackageId{id.mContainer, id.mPath}; + hiddenData[Callback::SCRIPT_NAME_KEY] = id.toString(); + return AsyncPackageId{id.mContainer, id.mPath, hiddenData}; }; return sol::make_object(context.mLua->sol(), initializer); } diff --git a/apps/openmw/mwlua/camerabindings.cpp b/apps/openmw/mwlua/camerabindings.cpp index 68f2331b9e..46bf079d65 100644 --- a/apps/openmw/mwlua/camerabindings.cpp +++ b/apps/openmw/mwlua/camerabindings.cpp @@ -7,7 +7,7 @@ namespace MWLua { sol::table api(context.mLua->sol(), sol::create); // TODO - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwlua/inputbindings.cpp b/apps/openmw/mwlua/inputbindings.cpp new file mode 100644 index 0000000000..aaa00f3da9 --- /dev/null +++ b/apps/openmw/mwlua/inputbindings.cpp @@ -0,0 +1,150 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwbase/inputmanager.hpp" +#include "../mwinput/actions.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + + sol::table initInputPackage(const Context& context) + { + sol::usertype keyEvent = context.mLua->sol().new_usertype("KeyEvent"); + keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) { 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["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); + keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); + keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); + keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); + + MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); + sol::table api(context.mLua->sol(), sol::create); + + api["isIdle"] = [input]() { return input->isIdle(); }; + api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); }; + api["isMouseButtonPressed"] = [input](int button) -> bool + { + return input->getMouseButtonsState() & (1 << (button - 1)); + }; + api["getMouseMoveX"] = [input]() { return input->getMouseMoveX(); }; + api["getMouseMoveY"] = [input]() { return input->getMouseMoveY(); }; + api["getAxisValue"] = [input](int axis) + { + if (axis < SDL_CONTROLLER_AXIS_MAX) + return input->getControllerAxisValue(static_cast(axis)); + else + return input->getActionValue(axis - SDL_CONTROLLER_AXIS_MAX) * 2 - 1; + }; + + api["getControlSwitch"] = [input](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 + )); + + return LuaUtil::makeReadOnly(api); + } + +} diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index d9bb5ff26e..8a1b76a8ce 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -22,11 +22,15 @@ namespace MWLua { using ActorControls = MWBase::LuaManager::ActorControls; sol::usertype controls = context.mLua->sol().new_usertype("ActorControls"); - controls["movement"] = &ActorControls::mMovement; - controls["sideMovement"] = &ActorControls::mSideMovement; - controls["turn"] = &ActorControls::mTurn; - controls["run"] = &ActorControls::mRun; - controls["jump"] = &ActorControls::mJump; + +#define CONTROL(TYPE, FIELD) sol::property([](const ActorControls& c) { return c.FIELD; },\ + [](ActorControls& c, const TYPE& v) { c.FIELD = v; c.mChanged = true; }) + controls["movement"] = CONTROL(float, mMovement); + controls["sideMovement"] = CONTROL(float, mSideMovement); + controls["turn"] = CONTROL(float, mTurn); + controls["run"] = CONTROL(bool, mRun); + controls["jump"] = CONTROL(bool, mJump); +#undef CONTROL sol::usertype selfAPI = context.mLua->sol().new_usertype("SelfObject", sol::base_classes, sol::bases()); @@ -34,7 +38,6 @@ namespace MWLua selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); }); selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; }; - selfAPI["setDirectControl"] = [](SelfObject& self, bool v) { self.mControls.mControlledFromLua = v; }; selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; }; selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment) { @@ -82,8 +85,6 @@ namespace MWLua LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) : LuaUtil::ScriptsContainer(lua, "L" + idToString(obj.id())), mData(obj) { - mData.mControls.mControlledFromLua = false; - mData.mControls.mDisableAI = false; this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({&mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers}); } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index ebb24401fc..6526a18367 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -1,21 +1,15 @@ #include "luabindings.hpp" -#include - #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" #include "../mwworld/inventorystore.hpp" #include "eventqueue.hpp" #include "worldview.hpp" -namespace sol -{ - template <> - struct is_automagical : std::false_type {}; -} - namespace MWLua { @@ -24,14 +18,20 @@ namespace MWLua sol::table res(lua.sol(), sol::create); for (const std::string& v : values) res[v] = v; - return lua.makeReadOnly(res); + return LuaUtil::makeReadOnly(res); } sol::table initCorePackage(const Context& context) { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_VERSION"] = 0; + api["API_REVISION"] = 6; + api["quit"] = [lua]() + { + std::string traceback = lua->sol()["debug"]["traceback"]().get(); + Log(Debug::Warning) << "Quit requested by a Lua script.\n" << traceback; + MWBase::Environment::get().getStateManager()->requestQuit(); + }; api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) { context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); @@ -43,7 +43,7 @@ namespace MWLua "Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient", "Light", "Miscellaneous", "NPC", "Player", "Potion", "Static", "Weapon" }); - api["EQUIPMENT_SLOT"] = lua->makeReadOnly(lua->sol().create_table_with( + 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, @@ -64,7 +64,7 @@ namespace MWLua "CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft, "Ammunition", MWWorld::InventoryStore::Slot_Ammunition )); - return lua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } sol::table initWorldPackage(const Context& context) @@ -107,37 +107,7 @@ namespace MWLua // return GObjectList{worldView->selectObjects(query, false)}; }; // TODO: add world.placeNewObject(recordId, cell, pos, [rot]) - return context.mLua->makeReadOnly(api); - } - - sol::table initNearbyPackage(const Context& context) - { - sol::table api(context.mLua->sol(), sol::create); - WorldView* worldView = context.mWorldView; - api["activators"] = LObjectList{worldView->getActivatorsInScene()}; - api["actors"] = LObjectList{worldView->getActorsInScene()}; - api["containers"] = LObjectList{worldView->getContainersInScene()}; - api["doors"] = LObjectList{worldView->getDoorsInScene()}; - api["items"] = LObjectList{worldView->getItemsInScene()}; - api["selectObjects"] = [context](const Queries::Query& query) - { - ObjectIdList list; - WorldView* worldView = context.mWorldView; - if (query.mQueryType == "activators") - list = worldView->getActivatorsInScene(); - else if (query.mQueryType == "actors") - list = worldView->getActorsInScene(); - else if (query.mQueryType == "containers") - list = worldView->getContainersInScene(); - else if (query.mQueryType == "doors") - list = worldView->getDoorsInScene(); - else if (query.mQueryType == "items") - list = worldView->getItemsInScene(); - return LObjectList{selectObjectsFromList(query, list, context)}; - // TODO: Maybe use sqlite - // return LObjectList{worldView->selectObjects(query, true)}; - }; - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } sol::table initQueryPackage(const Context& context) @@ -148,7 +118,7 @@ namespace MWLua query[t] = Queries::Query(std::string(t)); for (const QueryFieldGroup& group : getBasicQueryFieldGroups()) query[group.mName] = initFieldGroup(context, group); - return query; // makeReadonly is applied by LuaState::addCommonPackage + return query; // makeReadOnly is applied by LuaState::addCommonPackage } sol::table initFieldGroup(const Context& context, const QueryFieldGroup& group) @@ -163,24 +133,12 @@ namespace MWLua { const std::string& name = field->path()[i]; if (subgroup[name] == sol::nil) - subgroup[name] = context.mLua->makeReadOnly(context.mLua->newTable()); - subgroup = context.mLua->getMutableFromReadOnly(subgroup[name]); + subgroup[name] = LuaUtil::makeReadOnly(context.mLua->newTable()); + subgroup = LuaUtil::getMutableFromReadOnly(subgroup[name]); } subgroup[field->path().back()] = field; } - return context.mLua->makeReadOnly(res); - } - - void initInputBindings(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["withShift"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_SHIFT; }); - keyEvent["withCtrl"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_CTRL; }); - keyEvent["withAlt"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_ALT; }); - keyEvent["withSuper"] = sol::readonly_property([](const SDL_Keysym& e) -> bool { return e.mod & KMOD_GUI; }); + return LuaUtil::makeReadOnly(res); } } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index 8be96763a5..d1c62e43e3 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -21,12 +21,12 @@ namespace MWLua sol::table initCorePackage(const Context&); sol::table initWorldPackage(const Context&); - sol::table initNearbyPackage(const Context&); sol::table initQueryPackage(const Context&); sol::table initFieldGroup(const Context&, const QueryFieldGroup&); - void initInputBindings(const Context&); + // Implemented in nearbybindings.cpp + sol::table initNearbyPackage(const Context&); // Implemented in objectbindings.cpp void initObjectBindingsForLocalScripts(const Context&); @@ -47,9 +47,9 @@ namespace MWLua // Implemented in asyncbindings.cpp struct AsyncPackageId { - // TODO: add ObjectId mLocalObject; LuaUtil::ScriptsContainer* mContainer; std::string mScript; + sol::table mHiddenData; }; sol::function getAsyncPackageInitializer(const Context&); @@ -59,6 +59,14 @@ namespace MWLua // Implemented in uibindings.cpp sol::table initUserInterfacePackage(const Context&); + // Implemented in inputbindings.cpp + sol::table initInputPackage(const Context&); + + // Implemented in settingsbindings.cpp + sol::table initGlobalSettingsPackage(const Context&); + sol::table initLocalSettingsPackage(const Context&); + sol::table initPlayerSettingsPackage(const Context&); + // openmw.self package is implemented in localscripts.cpp } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index e01273bb84..38055c99b7 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -31,7 +31,10 @@ namespace MWLua mLocalLoader = createUserdataSerializer(true, mWorldView.getObjectRegistry(), &mContentFileMapping); mGlobalScripts.setSerializer(mGlobalSerializer.get()); + } + void LuaManager::init() + { Context context; context.mIsGlobal = true; context.mLuaManager = this; @@ -50,24 +53,35 @@ namespace MWLua initObjectBindingsForLocalScripts(localContext); initCellBindingsForLocalScripts(localContext); LocalScripts::initializeSelfPackage(localContext); - initInputBindings(localContext); mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context)); mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); mLua.addCommonPackage("openmw.core", initCorePackage(context)); mLua.addCommonPackage("openmw.query", initQueryPackage(context)); mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); + mGlobalScripts.addPackage("openmw.settings", initGlobalSettingsPackage(context)); mCameraPackage = initCameraPackage(localContext); mUserInterfacePackage = initUserInterfacePackage(localContext); + mInputPackage = initInputPackage(localContext); mNearbyPackage = initNearbyPackage(localContext); - } + mLocalSettingsPackage = initLocalSettingsPackage(localContext); + mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); - void LuaManager::init() - { - mKeyPressEvents.clear(); + mInputEvents.clear(); for (const std::string& path : mGlobalScriptList) if (mGlobalScripts.addNewScript(path)) Log(Debug::Info) << "Global script started: " << path; + 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) @@ -89,7 +103,7 @@ namespace MWLua if (paused) { - mKeyPressEvents.clear(); + mInputEvents.clear(); return; } @@ -122,14 +136,19 @@ namespace MWLua << ". Object not found or has no attached scripts"; } + // Run queued callbacks + for (CallbackWithData& c : mQueuedCallbacks) + c.mCallback(c.mArg); + mQueuedCallbacks.clear(); + // Engine handlers in local scripts PlayerScripts* playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); if (playerScripts) { - for (const SDL_Keysym& key : mKeyPressEvents) - playerScripts->keyPress(key); + for (const auto& event : mInputEvents) + playerScripts->processInputEvent(event); } - mKeyPressEvents.clear(); + mInputEvents.clear(); for (const LocalEngineEvent& e : mLocalEngineEvents) { @@ -183,7 +202,7 @@ namespace MWLua mActiveLocalScripts.clear(); mLocalEvents.clear(); mGlobalEvents.clear(); - mKeyPressEvents.clear(); + mInputEvents.clear(); mActorAddedEvents.clear(); mLocalEngineEvents.clear(); mPlayerChanged = false; @@ -198,6 +217,8 @@ namespace MWLua void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) { + if (!mInitialized) + return; if (!mPlayer.isEmpty()) throw std::logic_error("Player is initialized twice"); mWorldView.objectAddedToScene(ptr); @@ -247,11 +268,6 @@ namespace MWLua mWorldView.getObjectRegistry()->deregisterPtr(ptr); } - void LuaManager::keyPressed(const SDL_KeyboardEvent& arg) - { - mKeyPressEvents.push_back(arg.keysym); - } - void LuaManager::appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) { mLocalEngineEvents.push_back({getId(toPtr), LocalScripts::OnConsume{std::string(recordId)}}); @@ -279,17 +295,23 @@ namespace MWLua LocalScripts* LuaManager::createLocalScripts(const MWWorld::Ptr& ptr) { + assert(mInitialized); 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().getRefIdPtr() == "player") + if (ptr.getCellRef().getRefIdRef() == "player") { scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); scripts->addPackage("openmw.ui", mUserInterfacePackage); scripts->addPackage("openmw.camera", mCameraPackage); + scripts->addPackage("openmw.input", mInputPackage); + scripts->addPackage("openmw.settings", mPlayerSettingsPackage); } else + { scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry())); + scripts->addPackage("openmw.settings", mLocalSettingsPackage); + } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index df87457b23..91f48171f3 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -19,6 +19,19 @@ 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: @@ -41,7 +54,7 @@ namespace MWLua void objectRemovedFromScene(const MWWorld::Ptr& ptr) override; void registerObject(const MWWorld::Ptr& ptr) override; void deregisterObject(const MWWorld::Ptr& ptr) override; - void keyPressed(const SDL_KeyboardEvent &arg) override; + void inputEvent(const InputEvent& event) override { mInputEvents.push_back(event); } void appliedToObject(const MWWorld::Ptr& toPtr, std::string_view recordId, const MWWorld::Ptr& fromPtr) override; MWBase::LuaManager::ActorControls* getActorControls(const MWWorld::Ptr&) const override; @@ -67,13 +80,29 @@ namespace MWLua // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. 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)}); } + + // 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) + { + return [this, c](Arg arg) { this->queueCallback(c, sol::make_object(c.mFunc.lua_state(), arg)); }; + } + private: LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr); + bool mInitialized = false; LuaUtil::LuaState mLua; sol::table mNearbyPackage; sol::table mUserInterfacePackage; sol::table mCameraPackage; + sol::table mInputPackage; + sol::table mLocalSettingsPackage; + sol::table mPlayerSettingsPackage; std::vector mGlobalScriptList; GlobalScripts mGlobalScripts{&mLua}; @@ -93,9 +122,16 @@ namespace MWLua std::unique_ptr mGlobalLoader; std::unique_ptr mLocalLoader; - std::vector mKeyPressEvents; + std::vector mInputEvents; std::vector mActorAddedEvents; + struct CallbackWithData + { + Callback mCallback; + sol::object mArg; + }; + std::vector mQueuedCallbacks; + struct LocalEngineEvent { ObjectId mDest; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp new file mode 100644 index 0000000000..011d0ae9f3 --- /dev/null +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -0,0 +1,120 @@ +#include "luabindings.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwphysics/raycasting.hpp" + +#include "worldview.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; +} + +namespace MWLua +{ + sol::table initNearbyPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + WorldView* worldView = context.mWorldView; + + sol::usertype rayResult = + context.mLua->sol().new_usertype("RayCastingResult"); + rayResult["hit"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) { return r.mHit; }); + rayResult["hitPos"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHit) + return r.mHitPos; + else + return sol::nullopt; + }); + rayResult["hitNormal"] = sol::readonly_property([](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHit) + return r.mHitNormal; + else + return sol::nullopt; + }); + rayResult["hitObject"] = sol::readonly_property([worldView](const MWPhysics::RayCastingResult& r) -> sol::optional + { + if (r.mHitObject.isEmpty()) + return sol::nullopt; + else + return LObject(getId(r.mHitObject), worldView->getObjectRegistry()); + }); + + api["COLLISION_TYPE"] = LuaUtil::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["castRay"] = [](const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + MWWorld::Ptr ignore; + int collisionType = MWPhysics::CollisionType_Default; + float radius = 0; + if (options) + { + sol::optional ignoreObj = options->get>("ignore"); + if (ignoreObj) ignore = ignoreObj->ptr(); + collisionType = options->get>("collisionType").value_or(collisionType); + radius = options->get>("radius").value_or(0); + } + const MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + if (radius <= 0) + return rayCasting->castRay(from, to, ignore, std::vector(), collisionType); + else + { + if (!ignore.isEmpty()) throw std::logic_error("Currently castRay doesn't support `ignore` when radius > 0"); + return rayCasting->castSphere(from, to, radius, collisionType); + } + }; + // TODO: async raycasting + /*api["asyncCastRay"] = [luaManager = context.mLuaManager]( + const Callback& luaCallback, const osg::Vec3f& from, const osg::Vec3f& to, sol::optional options) + { + std::function callback = + luaManager->wrapLuaCallback(luaCallback); + MWPhysics::RayCastingInterface* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); + + // Handle options the same way as in `castRay`. + + // NOTE: `callback` is not thread safe. If MWPhysics works in separate thread, it must put results to a queue + // and use this callback from the main thread at the beginning of the next frame processing. + rayCasting->asyncCastRay(callback, from, to, ignore, std::vector(), collisionType); + };*/ + + api["activators"] = LObjectList{worldView->getActivatorsInScene()}; + api["actors"] = LObjectList{worldView->getActorsInScene()}; + api["containers"] = LObjectList{worldView->getContainersInScene()}; + api["doors"] = LObjectList{worldView->getDoorsInScene()}; + api["items"] = LObjectList{worldView->getItemsInScene()}; + api["selectObjects"] = [context](const Queries::Query& query) + { + ObjectIdList list; + WorldView* worldView = context.mWorldView; + if (query.mQueryType == "activators") + list = worldView->getActivatorsInScene(); + else if (query.mQueryType == "actors") + list = worldView->getActorsInScene(); + else if (query.mQueryType == "containers") + list = worldView->getContainersInScene(); + else if (query.mQueryType == "doors") + list = worldView->getDoorsInScene(); + else if (query.mQueryType == "items") + list = worldView->getItemsInScene(); + return LObjectList{selectObjectsFromList(query, list, context)}; + // TODO: Maybe use sqlite + // return LObjectList{worldView->selectObjects(query, true)}; + }; + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index 696179d003..d94ce1f49d 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -51,13 +51,13 @@ namespace MWLua bool isMarker(const MWWorld::Ptr& ptr) { - std::string_view id = *ptr.getCellRef().getRefIdPtr(); + std::string_view id = ptr.getCellRef().getRefIdRef(); return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; } std::string_view getMWClassName(const MWWorld::Ptr& ptr) { - if (*ptr.getCellRef().getRefIdPtr() == "player") + if (ptr.getCellRef().getRefIdRef() == "player") return "Player"; if (isMarker(ptr)) return "Marker"; @@ -71,7 +71,7 @@ namespace MWLua res.append(" ("); res.append(getMWClassName(ptr)); res.append(", "); - res.append(*ptr.getCellRef().getRefIdPtr()); + res.append(ptr.getCellRef().getRefIdRef()); res.append(")"); return res; } diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index d4ae041549..b7607c8b2c 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -158,6 +158,32 @@ namespace MWLua luaManager->addAction(std::move(action)); }; } + else + { // Only for local scripts + objectT["isOnGround"] = [](const ObjectT& o) + { + return MWBase::Environment::get().getWorld()->isOnGround(o.ptr()); + }; + objectT["isSwimming"] = [](const ObjectT& o) + { + return MWBase::Environment::get().getWorld()->isSwimming(o.ptr()); + }; + objectT["isInWeaponStance"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.isActor() && cls.getCreatureStats(o.ptr()).getDrawState() == MWMechanics::DrawState_Weapon; + }; + objectT["isInMagicStance"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.isActor() && cls.getCreatureStats(o.ptr()).getDrawState() == MWMechanics::DrawState_Spell; + }; + objectT["getCurrentSpeed"] = [](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getCurrentSpeed(o.ptr()); + }; + } } template diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index 72e064bb9b..ff0349b3c6 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -3,6 +3,8 @@ #include +#include "../mwbase/luamanager.hpp" + #include "localscripts.hpp" namespace MWLua @@ -13,13 +15,40 @@ namespace MWLua public: PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj) { - registerEngineHandlers({&mKeyPressHandlers}); + registerEngineHandlers({&mKeyPressHandlers, &mKeyReleaseHandlers, + &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, + &mActionHandlers}); } - void keyPress(const SDL_Keysym& key) { callEngineHandlers(mKeyPressHandlers, key); } + void processInputEvent(const MWBase::LuaManager::InputEvent& event) + { + using InputEvent = MWBase::LuaManager::InputEvent; + switch (event.mType) + { + case InputEvent::KeyPressed: + callEngineHandlers(mKeyPressHandlers, std::get(event.mValue)); + break; + case InputEvent::KeyReleased: + callEngineHandlers(mKeyReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerPressed: + callEngineHandlers(mControllerButtonPressHandlers, std::get(event.mValue)); + break; + case InputEvent::ControllerReleased: + callEngineHandlers(mControllerButtonReleaseHandlers, std::get(event.mValue)); + break; + case InputEvent::Action: + callEngineHandlers(mActionHandlers, std::get(event.mValue)); + break; + } + } private: EngineHandlerList mKeyPressHandlers{"onKeyPress"}; + EngineHandlerList mKeyReleaseHandlers{"onKeyRelease"}; + EngineHandlerList mControllerButtonPressHandlers{"onControllerButtonPress"}; + EngineHandlerList mControllerButtonReleaseHandlers{"onControllerButtonRelease"}; + EngineHandlerList mActionHandlers{"onInputAction"}; }; } diff --git a/apps/openmw/mwlua/settingsbindings.cpp b/apps/openmw/mwlua/settingsbindings.cpp new file mode 100644 index 0000000000..12dd69f73a --- /dev/null +++ b/apps/openmw/mwlua/settingsbindings.cpp @@ -0,0 +1,72 @@ +#include "luabindings.hpp" + +#include + +#include "../mwworld/esmstore.hpp" +#include "../mwworld/store.hpp" + +namespace MWLua +{ + + static sol::table initSettingsPackage(const Context& context, bool /*global*/, bool player) + { + LuaUtil::LuaState* lua = context.mLua; + sol::table config(lua->sol(), sol::create); + + // Access to settings.cfg. Temporary, will be removed at some point. + auto checkRead = [player](std::string_view category) + { + if ((category == "Camera" || category == "GUI" || category == "Hud" || + category == "Windows" || category == "Input") && !player) + throw std::runtime_error("This setting is only available in player scripts"); + }; + config["_getBoolFromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getBool(setting, category); + }; + config["_getIntFromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getInt(setting, category); + }; + config["_getFloatFromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getFloat(setting, category); + }; + config["_getStringFromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getString(setting, category); + }; + config["_getVector2FromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getVector2(setting, category); + }; + config["_getVector3FromSettingsCfg"] = [=](const std::string& category, const std::string& setting) + { + checkRead(category); + return Settings::Manager::getVector3(setting, category); + }; + + const MWWorld::Store* gmst = &MWBase::Environment::get().getWorld()->getStore().get(); + config["getGMST"] = [lua, gmst](const std::string setting) -> sol::object + { + const ESM::Variant& value = gmst->find(setting)->mValue; + if (value.getType() == ESM::VT_String) + return sol::make_object(lua->sol(), value.getString()); + else if (value.getType() == ESM::VT_Int) + return sol::make_object(lua->sol(), value.getInteger()); + else + return sol::make_object(lua->sol(), value.getFloat()); + }; + return LuaUtil::makeReadOnly(config); + } + + sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, true, false); } + sol::table initLocalSettingsPackage(const Context& context) { return initSettingsPackage(context, false, false); } + sol::table initPlayerSettingsPackage(const Context& context) { return initSettingsPackage(context, false, true); } + +} diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index cb14c41621..4fae84cd40 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -12,7 +12,7 @@ namespace MWLua { luaManager->addUIMessage(message); }; - return context.mLua->makeReadOnly(api); + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 319d797257..6ad6ef369e 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -5,97 +5,279 @@ #include +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "spelleffects.hpp" + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" + +namespace +{ + bool merge(std::vector& present, const std::vector& queued) + { + // Can't merge if we already have an effect with the same effect index + auto problem = std::find_if(queued.begin(), queued.end(), [&] (const auto& qEffect) + { + return std::find_if(present.begin(), present.end(), [&] (const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end(); + }); + if(problem != queued.end()) + return false; + present.insert(present.end(), queued.begin(), queued.end()); + return true; + } + + void addEffects(std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) + { + int currentEffectIndex = 0; + for(const auto& enam : list.mList) + { + ESM::ActiveEffect effect; + effect.mEffectId = enam.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mMagnMin; + effect.mMaxMagnitude = enam.mMagnMax; + effect.mEffectIndex = currentEffectIndex++; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if(ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + effect.mDuration = -1; + effect.mTimeLeft = -1; + effects.emplace_back(effect); + } + } +} namespace MWMechanics { - void ActiveSpells::update(float duration) const + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { - bool rebuild = false; + mActiveSpells.mIterating = true; + } - // Erase no longer active spells and effects - if (duration > 0) + ActiveSpells::IterationGuard::~IterationGuard() + { + mActiveSpells.mIterating = false; + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) + : mId(cast.mId), mDisplayName(cast.mSourceName), mCasterActorId(-1), mSlot(cast.mSlot), mType(cast.mType), mWorsenings(-1) + { + if(!caster.isEmpty() && caster.getClass().isActor()) + mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId(); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) + : mId(spell->mId), mDisplayName(spell->mName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()), mSlot(0) + , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability : ESM::ActiveSpells::Type_Permanent), mWorsenings(-1) + { + assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + addEffects(mEffects, spell->mEffects, ignoreResistances); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor) + : mId(item.getCellRef().getRefId()), mDisplayName(item.getClass().getName(item)), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) + , mSlot(slotIndex), mType(ESM::ActiveSpells::Type_Enchantment), mWorsenings(-1) + { + assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); + addEffects(mEffects, enchantment->mEffects); + } + + ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) + : mId(params.mId), mEffects(params.mEffects), mDisplayName(params.mDisplayName), mCasterActorId(params.mCasterActorId) + , mSlot(params.mItem.isSet() ? params.mItem.mIndex : 0) + , mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening}) + {} + + ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const + { + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = mId; + params.mEffects = mEffects; + params.mDisplayName = mDisplayName; + params.mCasterActorId = mCasterActorId; + params.mItem.unset(); + if(mSlot) { - TContainer::iterator iter (mSpells.begin()); - while (iter!=mSpells.end()) + // Note that we're storing the inventory slot as a RefNum instead of an int as a matter of future proofing + // mSlot needs to be replaced with a RefNum once inventory items get persistent RefNum (#4508 #6148) + params.mItem = { static_cast(mSlot), 0 }; + } + params.mType = mType; + params.mWorsenings = mWorsenings; + params.mNextWorsening = mNextWorsening.toEsm(); + return params; + } + + void ActiveSpells::ActiveSpellParams::worsen() + { + ++mWorsenings; + if(!mWorsenings) + mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); + mNextWorsening += CorprusStats::sWorseningPeriod; + } + + bool ActiveSpells::ActiveSpellParams::shouldWorsen() const + { + return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; + } + + void ActiveSpells::ActiveSpellParams::resetWorsenings() + { + mWorsenings = -1; + } + + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) + { + const auto& creatureStats = ptr.getClass().getCreatureStats(ptr); + assert(&creatureStats.getActiveSpells() == this); + IterationGuard guard{*this}; + // Erase no longer active spells and effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) + { + if(spellIt->mType != ESM::ActiveSpells::Type_Temporary && spellIt->mType != ESM::ActiveSpells::Type_Consumable) { - if (!timeToExpire (iter)) + ++spellIt; + continue; + } + bool removedSpell = false; + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f) { - mSpells.erase (iter++); - rebuild = true; + auto effect = *effectIt; + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + removedSpell = applyPurges(ptr, &spellIt, &effectIt); + if(removedSpell) + break; } else { - bool interrupt = false; - std::vector& effects = iter->second.mEffects; - for (std::vector::iterator effectIt = effects.begin(); effectIt != effects.end();) + ++effectIt; + } + } + if(removedSpell) + continue; + if(spellIt->mEffects.empty()) + spellIt = mSpells.erase(spellIt); + else + ++spellIt; + } + + for(const auto& spell : mQueue) + addToSpells(ptr, spell); + mQueue.clear(); + + // Vanilla only does this on cell change I think + const auto& spells = creatureStats.getSpells(); + for(const ESM::Spell* spell : spells) + { + if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + mSpells.emplace_back(ActiveSpellParams{spell, ptr}); + } + + if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) + { + auto& store = ptr.getClass().getInventoryStore(ptr); + if(store.getInvListener() != nullptr) + { + bool playNonLooping = !store.isFirstEquip(); + const auto world = MWBase::Environment::get().getWorld(); + for(int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) + { + auto slot = store.getSlot(slotIndex); + if(slot == store.end()) + continue; + const auto& enchantmentId = slot->getClass().getEnchantment(*slot); + if(enchantmentId.empty()) + continue; + const ESM::Enchantment* enchantment = world->getStore().get().find(enchantmentId); + if(enchantment->mData.mType != ESM::Enchantment::ConstantEffect) + continue; + if(std::find_if(mSpells.begin(), mSpells.end(), [&] (const ActiveSpellParams& params) { - if (effectIt->mTimeLeft <= 0) - { - rebuild = true; - - // Note: it we expire a Corprus effect, we should remove the whole spell. - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - iter = mSpells.erase (iter); - interrupt = true; - break; - } - - effectIt = effects.erase(effectIt); - } - else - { - effectIt->mTimeLeft -= duration; - ++effectIt; - } - } - - if (!interrupt) - ++iter; + return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId(); + }) != mSpells.end()) + continue; + ActiveSpellParams params(*slot, enchantment, slotIndex, ptr); + mSpells.emplace_back(params); + for(const auto& effect : params.mEffects) + MWMechanics::playEffects(ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); } } } - if (mSpellsChanged) + // Update effects + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - mSpellsChanged = false; - rebuild = true; - } + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid? + bool removedSpell = false; + for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) + { + bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + if(remove) + it = spellIt->mEffects.erase(it); + else + ++it; + removedSpell = applyPurges(ptr, &spellIt, &it); + if(removedSpell) + break; + } + if(removedSpell) + continue; - if (rebuild) - rebuildEffects(); + bool remove = false; + if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent) + remove = !spells.hasSpell(spellIt->mId); + else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment) + { + const auto& store = ptr.getClass().getInventoryStore(ptr); + auto slot = store.getSlot(spellIt->mSlot); + remove = slot == store.end() || slot->getCellRef().getRefId() != spellIt->mId; + } + if(remove) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + applyPurges(ptr, &spellIt); + continue; + } + ++spellIt; + } } - void ActiveSpells::rebuildEffects() const + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - mEffects = MagicEffects(); - - for (TIterator iter (begin()); iter!=end(); ++iter) + if(spell.mType != ESM::ActiveSpells::Type_Consumable) { - const std::vector& effects = iter->second.mEffects; - - for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + auto found = std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& existing) { - if (effectIt->mTimeLeft > 0) - mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude)); + return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId && spell.mSlot == existing.mSlot; + }); + if(found != mSpells.end()) + { + if(merge(found->mEffects, spell.mEffects)) + return; + auto params = *found; + mSpells.erase(found); + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); } } + mSpells.emplace_back(spell); } - ActiveSpells::ActiveSpells() - : mSpellsChanged (false) + ActiveSpells::ActiveSpells() : mIterating(false) {} - const MagicEffects& ActiveSpells::getMagicEffects() const - { - update(0.f); - return mEffects; - } - ActiveSpells::TIterator ActiveSpells::begin() const { return mSpells.begin(); @@ -106,246 +288,159 @@ namespace MWMechanics return mSpells.end(); } - double ActiveSpells::timeToExpire (const TIterator& iterator) const - { - const std::vector& effects = iterator->second.mEffects; - - float duration = 0; - - for (std::vector::const_iterator iter (effects.begin()); - iter!=effects.end(); ++iter) - { - if (iter->mTimeLeft > duration) - duration = iter->mTimeLeft; - } - - if (duration < 0) - return 0; - - return duration; - } - bool ActiveSpells::isSpellActive(const std::string& id) const { - for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) + return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell) { - if (Misc::StringUtils::ciEqual(iter->first, id)) - return true; - } - return false; + return Misc::StringUtils::ciEqual(spell.mId, id); + }) != mSpells.end(); } - const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const + void ActiveSpells::addSpell(const ActiveSpellParams& params) { - return mSpells; + mQueue.emplace_back(params); } - void ActiveSpells::addSpell(const std::string &id, bool stack, const std::vector& effects, - const std::string &displayName, int casterActorId) + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) { - TContainer::iterator it(mSpells.find(id)); - - ActiveSpellParams params; - params.mEffects = effects; - params.mDisplayName = displayName; - params.mCasterActorId = casterActorId; - - if (it == end() || stack) - { - mSpells.insert(std::make_pair(id, params)); - } - else - { - // addSpell() is called with effects for a range. - // but a spell may have effects with different ranges (e.g. Touch & Target) - // so, if we see new effects for same spell assume additional - // spell effects and add to existing effects of spell - mergeEffects(params.mEffects, it->second.mEffects); - it->second = params; - } - - mSpellsChanged = true; + mQueue.emplace_back(ActiveSpellParams{spell, actor, true}); } - void ActiveSpells::mergeEffects(std::vector& addTo, const std::vector& from) + void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) { - for (std::vector::const_iterator effect(from.begin()); effect != from.end(); ++effect) + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) { - // if effect is not in addTo, add it - bool missing = true; - for (std::vector::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter) + IterationGuard guard{*this}; + applyPurges(ptr); + } + } + + void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) + { + assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); + mPurges.emplace(predicate); + if(!mIterating) + { + IterationGuard guard{*this}; + applyPurges(ptr); + } + } + + bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, std::vector::iterator* currentEffect) + { + bool removedCurrentSpell = false; + while(!mPurges.empty()) + { + auto predicate = mPurges.front(); + mPurges.pop(); + for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg)) + bool isCurrentSpell = currentSpell && *currentSpell == spellIt; + std::visit([&] (auto&& variant) { - missing = false; - break; - } - } - if (missing) - { - addTo.push_back(*effect); + using T = std::decay_t; + if constexpr (std::is_same_v) + { + if(variant(*spellIt)) + { + auto params = *spellIt; + spellIt = mSpells.erase(spellIt); + if(isCurrentSpell) + { + *currentSpell = spellIt; + removedCurrentSpell = true; + } + for(const auto& effect : params.mEffects) + onMagicEffectRemoved(ptr, params, effect); + } + else + ++spellIt; + } + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); + for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();) + { + if(variant(*spellIt, *effectIt)) + { + auto effect = *effectIt; + if(isCurrentSpell && currentEffect) + { + auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect); + if(effectIt <= *currentEffect) + distance--; + effectIt = spellIt->mEffects.erase(effectIt); + *currentEffect = spellIt->mEffects.begin() + distance; + } + else + effectIt = spellIt->mEffects.erase(effectIt); + onMagicEffectRemoved(ptr, *spellIt, effect); + } + else + ++effectIt; + } + ++spellIt; + } + }, predicate); } } + return removedCurrentSpell; } - void ActiveSpells::removeEffects(const std::string &id) + void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const std::string &id) { - for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell) + purge([=] (const ActiveSpellParams& params) { - if (spell->first == id) - { - spell->second.mEffects.clear(); - mSpellsChanged = true; - } - } + return params.mId == id; + }, ptr); } - void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const + void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId) { - for (TContainer::const_iterator it = begin(); it != end(); ++it) + purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect) { - for (std::vector::const_iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end(); ++effectIt) - { - std::string name = it->second.mDisplayName; - - float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); - } - } + return effect.mEffectId == effectId; + }, ptr); } - void ActiveSpells::purgeAll(float chance, bool spellOnly) + void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ) + purge([=] (const ActiveSpellParams& params) { - const std::string spellId = it->first; - - // if spellOnly is true, dispell only spells. Leave potions, enchanted items etc. - if (spellOnly) - { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(spellId); - if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) - { - ++it; - continue; - } - } - - if (Misc::Rng::roll0to99() < chance) - mSpells.erase(it++); - else - ++it; - } - mSpellsChanged = true; + return params.mCasterActorId == casterActorId; + }, ptr); } - void ActiveSpells::purgeEffect(short effectId) + void ActiveSpells::clear(const MWWorld::Ptr& ptr) { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + mQueue.clear(); + purge([] (const ActiveSpellParams& params) { return true; }, ptr); + } + + void ActiveSpells::skipWorsenings(double hours) + { + for(auto& spell : mSpells) { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } + if(spell.mWorsenings >= 0) + spell.mNextWorsening += hours; } - mSpellsChanged = true; - } - - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) - { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } - } - mSpellsChanged = true; - } - - void ActiveSpells::purge(int casterActorId) - { - for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - for (std::vector::iterator effectIt = it->second.mEffects.begin(); - effectIt != it->second.mEffects.end();) - { - if (it->second.mCasterActorId == casterActorId) - effectIt = it->second.mEffects.erase(effectIt); - else - ++effectIt; - } - } - mSpellsChanged = true; - } - - void ActiveSpells::purgeCorprusDisease() - { - for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();) - { - bool hasCorprusEffect = false; - for (std::vector::iterator effectIt = iter->second.mEffects.begin(); - effectIt != iter->second.mEffects.end();++effectIt) - { - if (effectIt->mEffectId == ESM::MagicEffect::Corprus) - { - hasCorprusEffect = true; - break; - } - } - - if (hasCorprusEffect) - { - mSpells.erase(iter++); - mSpellsChanged = true; - } - else - ++iter; - } - } - - void ActiveSpells::clear() - { - mSpells.clear(); - mSpellsChanged = true; } void ActiveSpells::writeState(ESM::ActiveSpells &state) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ESM::ActiveSpells::ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - state.mSpells.insert (std::make_pair(it->first, params)); - } + for(const auto& spell : mSpells) + state.mSpells.emplace_back(spell.toEsm()); + for(const auto& spell : mQueue) + state.mQueue.emplace_back(spell.toEsm()); } void ActiveSpells::readState(const ESM::ActiveSpells &state) { - for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) - { - // Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp - ActiveSpellParams params; - params.mEffects = it->second.mEffects; - params.mCasterActorId = it->second.mCasterActorId; - params.mDisplayName = it->second.mDisplayName; - - mSpells.insert (std::make_pair(it->first, params)); - mSpellsChanged = true; - } + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + mSpells.emplace_back(ActiveSpellParams{spell}); + for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) + mQueue.emplace_back(ActiveSpellParams{spell}); } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 8bc29aa444..0538d7a8b7 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -1,40 +1,83 @@ #ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H -#include -#include +#include +#include +#include #include +#include +#include #include #include "../mwworld/timestamp.hpp" +#include "../mwworld/ptr.hpp" #include "magiceffects.hpp" +#include "spellcasting.hpp" + +namespace ESM +{ + struct Enchantment; + struct Spell; +} namespace MWMechanics { /// \brief Lasting spell effects /// - /// \note The name of this class is slightly misleading, since it also handels lasting potion + /// \note The name of this class is slightly misleading, since it also handles lasting potion /// effects. class ActiveSpells { public: - typedef ESM::ActiveEffect ActiveEffect; - - struct ActiveSpellParams + using ActiveEffect = ESM::ActiveEffect; + class ActiveSpellParams { - std::vector mEffects; - MWWorld::TimeStamp mTimeStamp; - std::string mDisplayName; + std::string mId; + std::vector mEffects; + std::string mDisplayName; + int mCasterActorId; + int mSlot; + ESM::ActiveSpells::EffectType mType; + int mWorsenings; + MWWorld::TimeStamp mNextWorsening; - // The caster that inflicted this spell on us - int mCasterActorId; + ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params); + + ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false); + + ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor); + + ESM::ActiveSpells::ActiveSpellParams toEsm() const; + + friend class ActiveSpells; + public: + ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + + const std::string& getId() const { return mId; } + + const std::vector& getEffects() const { return mEffects; } + std::vector& getEffects() { return mEffects; } + + ESM::ActiveSpells::EffectType getType() const { return mType; } + + int getCasterActorId() const { return mCasterActorId; } + + int getWorsenings() const { return mWorsenings; } + + const std::string& getDisplayName() const { return mDisplayName; } + + // Increments worsenings count and sets the next timestamp + void worsen(); + + bool shouldWorsen() const; + + void resetWorsenings(); }; - typedef std::multimap TContainer; - typedef TContainer::const_iterator TIterator; + typedef std::list::const_iterator TIterator; void readState (const ESM::ActiveSpells& state); void writeState (ESM::ActiveSpells& state) const; @@ -43,24 +86,29 @@ namespace MWMechanics TIterator end() const; - void update(float duration) const; + void update(const MWWorld::Ptr& ptr, float duration); private: + using ParamsPredicate = std::function; + using EffectPredicate = std::function; + using Predicate = std::variant; - mutable TContainer mSpells; - mutable MagicEffects mEffects; - mutable bool mSpellsChanged; + struct IterationGuard + { + ActiveSpells& mActiveSpells; - void rebuildEffects() const; + IterationGuard(ActiveSpells& spells); + ~IterationGuard(); + }; - /// Add any effects that are in "from" and not in "addTo" to "addTo" - void mergeEffects(std::vector& addTo, const std::vector& from); + std::list mSpells; + std::vector mQueue; + std::queue mPurges; + bool mIterating; - double timeToExpire (const TIterator& iterator) const; - ///< Returns time (in in-game hours) until the spell pointed to by \a iterator - /// expires. + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); - const TContainer& getActiveSpells() const; + bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); public: @@ -70,40 +118,31 @@ namespace MWMechanics /// /// \brief addSpell /// \param id ID for stacking purposes. - /// \param stack If false, the spell is not added if one with the same ID exists already. - /// \param effects - /// \param displayName Name for display in magic menu. /// - void addSpell (const std::string& id, bool stack, const std::vector& effects, - const std::string& displayName, int casterActorId); + void addSpell (const ActiveSpellParams& params); + + /// Bypasses resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects (const std::string& id); + void removeEffects (const MWWorld::Ptr& ptr, const std::string& id); /// Remove all active effects with this effect id - void purgeEffect (short effectId); + void purgeEffect (const MWWorld::Ptr& ptr, short effectId); - /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); + void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); + void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); - /// Remove all active effects, if roll succeeds (for each effect) - void purgeAll(float chance, bool spellOnly = false); - - /// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId - void purge (int casterActorId); + /// Remove all effects that were cast by \a casterActorId + void purge (const MWWorld::Ptr& ptr, int casterActorId); /// Remove all spells - void clear(); + void clear(const MWWorld::Ptr& ptr); bool isSpellActive (const std::string& id) const; ///< case insensitive - void purgeCorprusDisease(); - - const MagicEffects& getMagicEffects() const; - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - + void skipWorsenings(double hours); }; } diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp index a5c55633ac..5801ea751d 100644 --- a/apps/openmw/mwmechanics/actor.cpp +++ b/apps/openmw/mwmechanics/actor.cpp @@ -5,6 +5,7 @@ namespace MWMechanics { Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) + : mPositionAdjusted(false) { mCharacterController.reset(new CharacterController(ptr, animation)); } @@ -58,4 +59,14 @@ namespace MWMechanics { mIsTurningToPlayer = turning; } + + void Actor::setPositionAdjusted(bool adjusted) + { + mPositionAdjusted = adjusted; + } + + bool Actor::getPositionAdjusted() const + { + return mPositionAdjusted; + } } diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index be4f425378..c25fc1cb73 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -48,6 +48,9 @@ namespace MWMechanics return mEngageCombat.update(duration); } + void setPositionAdjusted(bool adjusted); + bool getPositionAdjusted() const; + private: std::unique_ptr mCharacterController; int mGreetingTimer{0}; @@ -55,6 +58,7 @@ namespace MWMechanics GreetingState mGreetingState{Greet_None}; bool mIsTurningToPlayer{false}; Misc::DeviatingPeriodicTimer mEngageCombat{1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f)}; + bool mPositionAdjusted; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 844b2b6987..bde18aa584 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -44,7 +44,6 @@ #include "actor.hpp" #include "summoning.hpp" #include "actorutil.hpp" -#include "tickableeffects.hpp" namespace { @@ -55,65 +54,26 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -int getBoundItemSlot (const std::string& itemId) +bool isCommanded(const MWWorld::Ptr& actor) { - static std::map boundItemsMap; - if (boundItemsMap.empty()) + const auto& stats = actor.getClass().getCreatureStats(actor); + for(const auto& params : stats.getActiveSpells()) { - std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; - - boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->mValue.getString(); - boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; + for(const auto& effect : params.getEffects()) + { + if(((effect.mEffectId == ESM::MagicEffect::CommandHumanoid && actor.getClass().isNpc()) + || (effect.mEffectId == ESM::MagicEffect::CommandCreature && !actor.getClass().isNpc())) + && effect.mMagnitude >= stats.getLevel()) + return true; + } } - - int slot = MWWorld::InventoryStore::Slot_CarriedRight; - std::map::iterator it = boundItemsMap.find(itemId); - if (it != boundItemsMap.end()) - slot = it->second; - - return slot; + return false; } -class CheckActorCommanded : public MWMechanics::EffectSourceVisitor -{ - MWWorld::Ptr mActor; -public: - bool mCommanded; - - CheckActorCommanded(const MWWorld::Ptr& actor) - : mActor(actor) - , mCommanded(false){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) - || (key.mId == ESM::MagicEffect::CommandCreature && mActor.getTypeName() == typeid(ESM::Creature).name())) - && magnitude >= mActor.getClass().getCreatureStats(mActor).getLevel()) - mCommanded = true; - } -}; - // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { - CheckActorCommanded check(actor); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - stats.getActiveSpells().visitEffectSources(check); bool hasCommandPackage = false; @@ -128,7 +88,7 @@ void adjustCommandedActor (const MWWorld::Ptr& actor) } } - if (!check.mCommanded && hasCommandPackage) + if (!isCommanded(actor) && hasCommandPackage) stats.getAiSequence().erase(it); } @@ -167,129 +127,43 @@ void forEachFollowingPackage(MWMechanics::Actors::PtrActorMap& actors, const MWW } } -} - -namespace MWMechanics +float getStuntedMagickaDuration(const MWWorld::Ptr& actor) { - static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player - static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player - static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement - static const float DECELERATE_DISTANCE = 512.f; - - namespace + float remainingTime = 0.f; + for(const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { - float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) + for(const auto& effect : params.getEffects()) { - const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); - return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; - } - } - - class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor - { - public: - float mRemainingTime; - - GetStuntedMagickaDuration(const MWWorld::Ptr& actor) - : mRemainingTime(0.f){} - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mRemainingTime == -1) return; - - if (key.mId == ESM::MagicEffect::StuntedMagicka) + if(effect.mEffectId == ESM::MagicEffect::StuntedMagicka) { - if (totalTime == -1) - { - mRemainingTime = -1; - return; - } - - if (remainingTime > mRemainingTime) - mRemainingTime = remainingTime; + if(effect.mDuration == -1.f) + return -1.f; + remainingTime = std::max(remainingTime, effect.mTimeLeft); } } - }; + } + return remainingTime; +} - class GetCurrentMagnitudes : public MWMechanics::EffectSourceVisitor +void soulTrap(const MWWorld::Ptr& creature) +{ + const auto& stats = creature.getClass().getCreatureStats(creature); + if(!stats.getMagicEffects().get(ESM::MagicEffect::Soultrap).getMagnitude()) + return; + int creatureSoulValue = creature.get()->mBase->mData.mSoul; + if (creatureSoulValue == 0) + return; + MWBase::World* world = MWBase::Environment::get().getWorld(); + static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); + for(const auto& params : stats.getActiveSpells()) { - std::string mSpellId; - - public: - GetCurrentMagnitudes(const std::string& spellId) - : mSpellId(spellId) + for(const auto& effect : params.getEffects()) { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (magnitude <= 0) - return; - - if (sourceId != mSpellId) - return; - - mMagnitudes.emplace_back(key, magnitude); - } - - std::vector> mMagnitudes; - }; - - class GetCorprusSpells : public MWMechanics::EffectSourceVisitor - { - - public: - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (key.mId != ESM::MagicEffect::Corprus) - return; - - mSpells.push_back(sourceId); - } - - std::vector mSpells; - }; - - class SoulTrap : public MWMechanics::EffectSourceVisitor - { - MWWorld::Ptr mCreature; - MWWorld::Ptr mActor; - bool mTrapped; - public: - SoulTrap(const MWWorld::Ptr& trappedCreature) - : mCreature(trappedCreature) - , mTrapped(false) - { - } - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override - { - if (mTrapped) - return; - if (key.mId != ESM::MagicEffect::Soultrap) - return; - if (magnitude <= 0) - return; - - MWBase::World* world = MWBase::Environment::get().getWorld(); - - MWWorld::Ptr caster = world->searchPtrViaActorId(casterActorId); + if(effect.mEffectId != ESM::MagicEffect::Soultrap || effect.mMagnitude <= 0.f) + continue; + MWWorld::Ptr caster = world->searchPtrViaActorId(params.getCasterActorId()); if (caster.isEmpty() || !caster.getClass().isActor()) - return; - - static const float fSoulgemMult = world->getStore().get().find("fSoulgemMult")->mValue.getFloat(); - - int creatureSoulValue = mCreature.get()->mBase->mData.mSoul; - if (creatureSoulValue == 0) - return; + continue; // Use the smallest soulgem that is large enough to hold the soul MWWorld::ContainerStore& container = caster.getClass().getContainerStore(caster); @@ -314,126 +188,58 @@ namespace MWMechanics } if (gem == container.end()) - return; + continue; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem, caster); - gem->getCellRef().setSoul(mCreature.getCellRef().getRefId()); + gem->getCellRef().setSoul(creature.getCellRef().getRefId()); // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); - mTrapped = true; - - if (caster == getPlayer()) + if (caster == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() + const ESM::Static* fx = world->getStore().get() .search("VFX_Soul_Trap"); if (fx) - MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + fx->mModel, - "", mCreature.getRefData().getPosition().asVec3()); + world->spawnEffect("meshes\\" + fx->mModel, "", creature.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getSoundManager()->playSound3D( - mCreature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f - ); + MWBase::Environment::get().getSoundManager()->playSound3D(creature.getRefData().getPosition().asVec3(), "conjuration hit", 1.f, 1.f); + return; //remove to get vanilla behaviour } - }; - - void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); - - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; - - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); - - if (actor != MWMechanics::getPlayer()) - return; - - MWWorld::Ptr newItem; - auto it = store.getSlot(slot); - // Equip can fail because beast races cannot equip boots/helmets - if(it != store.end()) - newItem = *it; - - if (newItem.isEmpty() || boundPtr != newItem) - return; - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState_Weapon); - - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } +} - void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) +void removeTemporaryEffects(const MWWorld::Ptr& ptr) +{ + ptr.getClass().getCreatureStats(ptr).getActiveSpells().purge([] (const auto& spell) { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - int slot = getBoundItemSlot(itemId); + return spell.getType() == ESM::ActiveSpells::Type_Consumable || spell.getType() == ESM::ActiveSpells::Type_Temporary; + }, ptr); +} +} - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); +namespace MWMechanics +{ + static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player + static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player + static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement + static const float DECELERATE_DISTANCE = 512.f; - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); - - if (actor != MWMechanics::getPlayer()) + namespace + { + float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration, const osg::Vec3f& halfExtents) { - store.remove(itemId, 1, actor); - - // Equip a replacement - if (!wasEquipped) - return; - - std::string type = currentItem->getTypeName(); - if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) - return; - - if (actor.getClass().getCreatureStats(actor).isDead()) - return; - - if (!actor.getClass().hasInventoryStore(actor)) - return; - - if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) - return; - - actor.getClass().getInventoryStore(actor).autoEquip(actor); - - return; + const auto distanceToNextPathPoint = (package.getNextPathPoint(package.getDestination()) - position).length(); + return (distanceToNextPathPoint - package.getNextPathPointTolerance(speed, duration, halfExtents)) / speed; } - - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player.getPreviousItem(itemId); - player.erasePreviousItem(itemId); - - if (!prevItemId.empty()) - { - // Find previous item (or its replacement) by id. - // we should equip previous item only if expired bound item was equipped. - MWWorld::Ptr item = store.findReplacement(prevItemId); - if (!item.isEmpty() && wasEquipped) - { - MWWorld::ActionEquip action(item); - action.execute(actor); - } - } - - store.remove(itemId, 1, actor); } void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects - adjustMagicEffects (ptr); + adjustMagicEffects (ptr, duration); if (ptr.getClass().getCreatureStats(ptr).needToRecalcDynamicStats()) calculateDynamicStats (ptr); @@ -791,23 +597,61 @@ namespace MWMechanics } } - void Actors::adjustMagicEffects (const MWWorld::Ptr& creature) + void Actors::adjustMagicEffects (const MWWorld::Ptr& creature, float duration) { - CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); - if (creatureStats.isDeathAnimationFinished()) - return; + CreatureStats& creatureStats = creature.getClass().getCreatureStats (creature); + bool wasDead = creatureStats.isDead(); - MagicEffects now = creatureStats.getSpells().getMagicEffects(); + creatureStats.getActiveSpells().update(creature, duration); - if (creature.getClass().hasInventoryStore(creature)) + if (!wasDead && creatureStats.isDead()) { - MWWorld::InventoryStore& store = creature.getClass().getInventoryStore (creature); - now += store.getMagicEffects(); + // The actor was killed by a magic effect. Figure out if the player was responsible for it. + const ActiveSpells& spells = creatureStats.getActiveSpells(); + MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) + { + bool actorKilled = false; + + const ActiveSpells::ActiveSpellParams& spell = *it; + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); + for (const auto& effect : spell.getEffects()) + { + static const std::array damageEffects{ + ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, + ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth + }; + + bool isDamageEffect = std::find(damageEffects.begin(), damageEffects.end(), effect.mEffectId) != damageEffects.end(); + + if (isDamageEffect) + { + if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); + actorKilled = true; + break; + } + } + } + + if (actorKilled) + break; + } } - now += creatureStats.getActiveSpells().getMagicEffects(); + // updateSummons assumes the actor belongs to a cell. + // This assumption isn't always valid for the player character. + if (!creature.isInCell()) + return; - creatureStats.modifyMagicEffects(now); + if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) + updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } void Actors::calculateDynamicStats (const MWWorld::Ptr& ptr) @@ -855,22 +699,18 @@ namespace MWMechanics if (stunted) { // Stunted Magicka effect should be taken into account. - GetStuntedMagickaDuration visitor(ptr); - stats.getActiveSpells().visitEffectSources(visitor); - stats.getSpells().visitEffectSources(visitor); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(visitor); + float remainingTime = getStuntedMagickaDuration(ptr); // Take a maximum remaining duration of Stunted Magicka effects (-1 is a constant one) in game hours. - if (visitor.mRemainingTime > 0) + if (remainingTime > 0) { double timeScale = MWBase::Environment::get().getWorld()->getTimeScaleFactor(); if(timeScale == 0.0) timeScale = 1; - restoreHours = std::max(0.0, hours - visitor.mRemainingTime * timeScale / 3600.f); + restoreHours = std::max(0.0, hours - remainingTime * timeScale / 3600.f); } - else if (visitor.mRemainingTime == -1) + else if (remainingTime == -1) restoreHours = 0; } @@ -931,404 +771,12 @@ namespace MWMechanics stats.setFatigue (fatigue); } - class ExpiryVisitor : public EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - float mDuration; - - public: - ExpiryVisitor(const MWWorld::Ptr& actor, float duration) - : mActor(actor), mDuration(duration) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) override - { - if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration) - { - CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - if (effectTick(creatureStats, mActor, key, magnitude * remainingTime)) - creatureStats.getMagicEffects().add(key, -magnitude); - } - } - }; - - void Actors::applyCureEffects(const MWWorld::Ptr& actor) - { - CreatureStats &creatureStats = actor.getClass().getCreatureStats(actor); - const MagicEffects &effects = creatureStats.getMagicEffects(); - - if (effects.get(ESM::MagicEffect::CurePoison).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Poison); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Poison); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison); - } - if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze); - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze); - } - if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeCommonDisease(); - } - if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0) - { - creatureStats.getSpells().purgeBlightDisease(); - } - if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0) - { - creatureStats.getActiveSpells().purgeCorprusDisease(); - creatureStats.getSpells().purgeCorprusDisease(); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true); - } - if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0) - { - creatureStats.getSpells().purgeCurses(); - } - } - void Actors::calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration) { CreatureStats &creatureStats = ptr.getClass().getCreatureStats(ptr); - const MagicEffects &effects = creatureStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - applyCureEffects(ptr); - - bool wasDead = creatureStats.isDead(); - - if (duration > 0) - { - // Apply correct magnitude for tickable effects that have just expired, - // in case duration > remaining time of effect. - // One case where this will happen is when the player uses the rest/wait command - // while there is a tickable effect active that should expire before the end of the rest/wait. - ExpiryVisitor visitor(ptr, duration); - creatureStats.getActiveSpells().visitEffectSources(visitor); - - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - // tickable effects (i.e. effects having a lasting impact after expiry) - effectTick(creatureStats, ptr, it->first, it->second.getMagnitude() * duration); - - // instant effects are already applied on spell impact in spellcasting.cpp, but may also come from permanent abilities - if (it->second.getMagnitude() > 0) - { - CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, it->first, it->second.getMagnitude())) - { - creatureStats.getSpells().purgeEffect(it->first.mId); - creatureStats.getActiveSpells().purgeEffect(it->first.mId); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(it->first.mId); - } - } - } - } - - // purge levitate effect if levitation is disabled - // check only modifier, because base value can be setted from SetFlying console command. - if (MWBase::Environment::get().getWorld()->isLevitationEnabled() == false && effects.get(ESM::MagicEffect::Levitate).getModifier() > 0) - { - creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Levitate); - creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Levitate); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).purgeEffect(ESM::MagicEffect::Levitate); - - if (ptr == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); - } - } - - // dynamic stats - for (int i = 0; i < 3; ++i) - { - DynamicStat stat = creatureStats.getDynamic(i); - float fortify = effects.get(ESM::MagicEffect::FortifyHealth + i).getMagnitude(); - float drain = 0.f; - if (!godmode) - drain = effects.get(ESM::MagicEffect::DrainHealth + i).getMagnitude(); - stat.setCurrentModifier(fortify - drain, - // Magicka can be decreased below zero due to a fortify effect wearing off - // Fatigue can be decreased below zero meaning the actor will be knocked out - i == 1 || i == 2); - - creatureStats.setDynamic(i, stat); - } - - // attributes - for(int i = 0;i < ESM::Attribute::Length;++i) - { - AttributeValue stat = creatureStats.getAttribute(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifyAttribute, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainAttribute, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbAttribute, i)).getMagnitude(); - } - stat.setModifier(static_cast(fortify - drain - absorb)); - - creatureStats.setAttribute(i, stat); - } if (creatureStats.needToRecalcDynamicStats()) calculateDynamicStats(ptr); - - if (ptr == getPlayer()) - { - GetCorprusSpells getCorprusSpellsVisitor; - creatureStats.getSpells().visitEffectSources(getCorprusSpellsVisitor); - creatureStats.getActiveSpells().visitEffectSources(getCorprusSpellsVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getCorprusSpellsVisitor); - std::vector corprusSpells = getCorprusSpellsVisitor.mSpells; - std::vector corprusSpellsToRemove; - - for (auto it = creatureStats.getCorprusSpells().begin(); it != creatureStats.getCorprusSpells().end(); ++it) - { - if(std::find(corprusSpells.begin(), corprusSpells.end(), it->first) == corprusSpells.end()) - { - // Corprus effect expired, remove entry and restore stats. - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, it->first); - corprusSpellsToRemove.push_back(it->first); - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - continue; - } - - corprusSpells.erase(std::remove(corprusSpells.begin(), corprusSpells.end(), it->first), corprusSpells.end()); - - if (MWBase::Environment::get().getWorld()->getTimeStamp() >= it->second.mNextWorsening) - { - it->second.mNextWorsening += CorprusStats::sWorseningPeriod; - GetCurrentMagnitudes getMagnitudesVisitor (it->first); - creatureStats.getSpells().visitEffectSources(getMagnitudesVisitor); - creatureStats.getActiveSpells().visitEffectSources(getMagnitudesVisitor); - ptr.getClass().getInventoryStore(ptr).visitEffectSources(getMagnitudesVisitor); - for (auto& effectMagnitude : getMagnitudesVisitor.mMagnitudes) - { - if (effectMagnitude.first.mId == ESM::MagicEffect::FortifyAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - attr.damage(-effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - it->second.mWorsenings[effectMagnitude.first.mArg] = 0; - } - else if (effectMagnitude.first.mId == ESM::MagicEffect::DrainAttribute) - { - AttributeValue attr = creatureStats.getAttribute(effectMagnitude.first.mArg); - int currentDamage = attr.getDamage(); - if (currentDamage >= 0) - it->second.mWorsenings[effectMagnitude.first.mArg] = std::min(it->second.mWorsenings[effectMagnitude.first.mArg], currentDamage); - - it->second.mWorsenings[effectMagnitude.first.mArg] += effectMagnitude.second; - attr.damage(effectMagnitude.second); - creatureStats.setAttribute(effectMagnitude.first.mArg, attr); - } - } - - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); - } - } - - for (std::string& oldCorprusSpell : corprusSpellsToRemove) - { - creatureStats.removeCorprusSpell(oldCorprusSpell); - } - - for (std::string& newCorprusSpell : corprusSpells) - { - CorprusStats corprus; - for (int i=0; igetTimeStamp() + CorprusStats::sWorseningPeriod; - - creatureStats.addCorprusSpell(newCorprusSpell, corprus); - } - } - - // AI setting modifiers - int creature = !ptr.getClass().isNpc(); - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Humanoid) - creature = false; - // Note: the Creature variants only work on normal creatures, not on daedra or undead creatures. - if (!creature || ptr.get()->mBase->mData.mType == ESM::Creature::Creatures) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Fight); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::FrenzyHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::CalmHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Fight, stat); - - stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::DemoralizeHumanoid + creature).getMagnitude() - - effects.get(ESM::MagicEffect::RallyHumanoid+creature).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - if (creature && ptr.get()->mBase->mData.mType == ESM::Creature::Undead) - { - Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); - stat.setModifier(static_cast(effects.get(ESM::MagicEffect::TurnUndead).getMagnitude())); - creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); - } - - if (!wasDead && creatureStats.isDead()) - { - // The actor was killed by a magic effect. Figure out if the player was responsible for it. - const ActiveSpells& spells = creatureStats.getActiveSpells(); - MWWorld::Ptr player = getPlayer(); - std::set playerFollowers; - getActorsSidingWith(player, playerFollowers); - - for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) - { - bool actorKilled = false; - - const ActiveSpells::ActiveSpellParams& spell = it->second; - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - for (std::vector::const_iterator effectIt = spell.mEffects.begin(); - effectIt != spell.mEffects.end(); ++effectIt) - { - int effectId = effectIt->mEffectId; - bool isDamageEffect = false; - - int damageEffects[] = { - ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, - ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth - }; - - for (unsigned int i=0; iactorKilled(ptr, player); - actorKilled = true; - break; - } - } - } - - if (actorKilled) - break; - } - } - - // TODO: dirty flag for magic effects to avoid some unnecessary work below? - - // any value of calm > 0 will stop the actor from fighting - if ((effects.get(ESM::MagicEffect::CalmHumanoid).getMagnitude() > 0 && ptr.getClass().isNpc()) - || (effects.get(ESM::MagicEffect::CalmCreature).getMagnitude() > 0 && !ptr.getClass().isNpc())) - creatureStats.getAiSequence().stopCombat(); - - // Update bound effects - // Note: in vanilla MW multiple bound items of the same type can be created by different spells. - // As these extra copies are kinda useless this may or may not be important. - static std::map boundItemsMap; - if (boundItemsMap.empty()) - { - boundItemsMap[ESM::MagicEffect::BoundBattleAxe] = "sMagicBoundBattleAxeID"; - boundItemsMap[ESM::MagicEffect::BoundBoots] = "sMagicBoundBootsID"; - boundItemsMap[ESM::MagicEffect::BoundCuirass] = "sMagicBoundCuirassID"; - boundItemsMap[ESM::MagicEffect::BoundDagger] = "sMagicBoundDaggerID"; - boundItemsMap[ESM::MagicEffect::BoundGloves] = "sMagicBoundLeftGauntletID"; // Note: needs RightGauntlet variant too (see below) - boundItemsMap[ESM::MagicEffect::BoundHelm] = "sMagicBoundHelmID"; - boundItemsMap[ESM::MagicEffect::BoundLongbow] = "sMagicBoundLongbowID"; - boundItemsMap[ESM::MagicEffect::BoundLongsword] = "sMagicBoundLongswordID"; - boundItemsMap[ESM::MagicEffect::BoundMace] = "sMagicBoundMaceID"; - boundItemsMap[ESM::MagicEffect::BoundShield] = "sMagicBoundShieldID"; - boundItemsMap[ESM::MagicEffect::BoundSpear] = "sMagicBoundSpearID"; - } - - if(ptr.getClass().hasInventoryStore(ptr)) - { - for (const auto& [effect, itemGmst] : boundItemsMap) - { - bool found = creatureStats.mBoundItems.find(effect) != creatureStats.mBoundItems.end(); - float magnitude = effects.get(effect).getMagnitude(); - if (found != (magnitude > 0)) - { - if (magnitude > 0) - creatureStats.mBoundItems.insert(effect); - else - creatureStats.mBoundItems.erase(effect); - - std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( - itemGmst)->mValue.getString(); - - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - - if (effect == ESM::MagicEffect::BoundGloves) - { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundRightGauntletID")->mValue.getString(); - magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); - } - } - } - } - - // Summoned creature update visitor assumes the actor belongs to a cell. - // This assumption isn't always valid for the player character. - if (!ptr.isInCell()) - return; - - bool hasSummonEffect = false; - for (MagicEffects::Collection::const_iterator it = effects.begin(); it != effects.end(); ++it) - { - if (isSummoningEffect(it->first.mId)) - { - hasSummonEffect = true; - break; - } - } - - if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty() || hasSummonEffect) - { - UpdateSummonedCreatures updateSummonedCreatures(ptr); - creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); - creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); - if (ptr.getClass().hasInventoryStore(ptr)) - ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); - updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); - } - } - - void Actors::calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration) - { - NpcStats &npcStats = ptr.getClass().getNpcStats(ptr); - const MagicEffects &effects = npcStats.getMagicEffects(); - bool godmode = ptr == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - // skills - for(int i = 0;i < ESM::Skill::Length;++i) - { - SkillValue& skill = npcStats.getSkill(i); - float fortify = effects.get(EffectKey(ESM::MagicEffect::FortifySkill, i)).getMagnitude(); - float drain = 0.f, absorb = 0.f; - if (!godmode) - { - drain = effects.get(EffectKey(ESM::MagicEffect::DrainSkill, i)).getMagnitude(); - absorb = effects.get(EffectKey(ESM::MagicEffect::AbsorbSkill, i)).getMagnitude(); - } - skill.setModifier(static_cast(fortify - drain - absorb)); - } } bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr) @@ -1610,12 +1058,12 @@ namespace MWMechanics void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately) { - removeActor(ptr); + removeActor(ptr, true); MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; - mActors.insert(std::make_pair(ptr, new Actor(ptr, anim))); + mActors.emplace(ptr, new Actor(ptr, anim)); CharacterController* ctrl = mActors[ptr]->getCharacterController(); if (updateImmediately) @@ -1658,11 +1106,13 @@ namespace MWMechanics ctrl->setVisibility(visibilityRatio); } - void Actors::removeActor (const MWWorld::Ptr& ptr) + void Actors::removeActor (const MWWorld::Ptr& ptr, bool keepActive) { PtrActorMap::iterator iter = mActors.find(ptr); if(iter != mActors.end()) { + if(!keepActive) + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter); } @@ -1727,6 +1177,7 @@ namespace MWMechanics { if((iter->first.isInCell() && iter->first.getCell()==cellStore) && iter->first != ignore) { + removeTemporaryEffects(iter->first); delete iter->second; mActors.erase(iter++); } @@ -1811,6 +1262,7 @@ namespace MWMechanics // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either // follow player or have a AIWander package with non-empty wander area. bool shouldAvoidCollision = isMoving; + bool shouldGiveWay = false; bool shouldTurnToApproachingActor = !isMoving; MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). const auto& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); @@ -1821,7 +1273,7 @@ namespace MWMechanics else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { if (!static_cast(package.get())->isStationary()) - shouldAvoidCollision = true; + shouldGiveWay = true; } else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) { @@ -1831,23 +1283,23 @@ namespace MWMechanics break; } } - if (!shouldAvoidCollision) + if (!shouldAvoidCollision && !shouldGiveWay) continue; osg::Vec2f baseSpeed = origMovement * maxSpeed; osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); float baseRotZ = ptr.getRefData().getPosition().rot[2]; - osg::Vec3f halfExtents = world->getHalfExtents(ptr); + const osg::Vec3f halfExtents = world->getHalfExtents(ptr); float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; - float timeToCollision = maxTimeToCheck; + float timeToCheck = maxTimeToCheck; + if (!shouldGiveWay && !aiSequence.isEmpty()) + timeToCheck = std::min(timeToCheck, getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents)); + + float timeToCollision = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; - const float timeToDestination = aiSequence.isEmpty() - ? std::numeric_limits::max() - : getTimeToDestination(**aiSequence.begin(), basePos, maxSpeed, duration, halfExtents); - // Iterate through all other actors and predict collisions. for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) { @@ -1855,7 +1307,7 @@ namespace MWMechanics if (otherPtr == ptr || otherPtr == currentTarget) continue; - osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); float dist = deltaPos.length(); @@ -1873,7 +1325,7 @@ namespace MWMechanics float rotZ = otherPtr.getRefData().getPosition().rot[2]; osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; - float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x(); + float collisionDist = minGap + halfExtents.x() + otherHalfExtents.x(); collisionDist = std::min(collisionDist, relPos.length()); // Find the earliest `t` when |relPos + relSpeed * t| == collisionDist. @@ -1884,7 +1336,7 @@ namespace MWMechanics continue; // No solution; distance is always >= collisionDist. float t = (-vr - std::sqrt(Dh)) / v2; - if (t < 0 || t > timeToCollision || t > timeToDestination) + if (t < 0 || t > timeToCollision) continue; // Check visibility and awareness last as it's expensive. @@ -1904,7 +1356,7 @@ namespace MWMechanics movementCorrection.y() *= 0.5f; } - if (timeToCollision < maxTimeToCheck) + if (timeToCollision < timeToCheck) { // Try to evade the nearest collision. osg::Vec2f newMovement = origMovement + movementCorrection; @@ -1982,8 +1434,6 @@ namespace MWMechanics player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - const Misc::TimerStatus engageCombatTimerStatus = iter->second->updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles @@ -1991,7 +1441,7 @@ namespace MWMechanics { // They can be added during the death animation if (!iter->first.getClass().getCreatureStats(iter->first).isDeathAnimationFinished()) - adjustMagicEffects(iter->first); + adjustMagicEffects(iter->first, duration); ctrl->updateContinuousVfx(); } else @@ -2086,8 +1536,6 @@ namespace MWMechanics if (inProcessingRange) updateDrowning(iter->first, duration, ctrl->isKnockedOut(), isPlayer); - calculateNpcStatModifiers(iter->first, duration); - if (timerUpdateEquippedLight == 0) updateEquippedLight(iter->first, updateEquippedLightInterval, showTorches); } @@ -2101,7 +1549,7 @@ namespace MWMechanics float rotationZ = mov.mRotation[2]; bool jump = mov.mPosition[2] == 1; bool runFlag = stats.getMovementFlag(MWMechanics::CreatureStats::Flag_Run); - if (luaControls->mControlledFromLua) + if (luaControls->mChanged) { mov.mPosition[0] = luaControls->mSideMovement; mov.mPosition[1] = luaControls->mMovement; @@ -2110,6 +1558,7 @@ namespace MWMechanics mov.mRotation[2] = luaControls->mTurn; mov.mSpeedFactor = osg::Vec2(luaControls->mMovement, luaControls->mSideMovement).length(); stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, luaControls->mRun); + luaControls->mChanged = false; } luaControls->mSideMovement = movement.x(); luaControls->mMovement = movement.y(); @@ -2159,7 +1608,14 @@ namespace MWMechanics continue; } else if (!isPlayer) + { iter->first.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); + if (!iter->second->getPositionAdjusted()) + { + iter->first.getClass().adjustPosition(iter->first, false); + iter->second->setPositionAdjusted(true); + } + } const bool isDead = iter->first.getClass().getCreatureStats(iter->first).isDead(); if (!isDead && (!godmode || !isPlayer) && iter->first.getClass().getCreatureStats(iter->first).isParalyzed()) @@ -2253,10 +1709,7 @@ namespace MWMechanics // Apply soultrap if (iter->first.getTypeName() == typeid(ESM::Creature).name()) - { - SoulTrap soulTrap (iter->first); - stats.getActiveSpells().visitEffectSources(soulTrap); - } + soulTrap(iter->first); // Magic effects will be reset later, and the magic effect that could kill the actor // needs to be determined now @@ -2272,30 +1725,27 @@ namespace MWMechanics // Reset magic effects and recalculate derived effects // One case where we need this is to make sure bound items are removed upon death - stats.modifyMagicEffects(MWMechanics::MagicEffects()); - stats.getActiveSpells().clear(); + float vampirism = stats.getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude(); + stats.getActiveSpells().clear(iter->first); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); // Reset dynamic stats, attributes and skills calculateCreatureStatModifiers(iter->first, 0); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, 0); + stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { //player's death animation is over MWBase::Environment::get().getStateManager()->askLoadRecent(); + // Play Death Music if it was the player dying + MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } else { // NPC death animation is over, disable actor collision MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false); } - - // Play Death Music if it was the player dying - if(iter->first == getPlayer()) - MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3"); } } } @@ -2315,7 +1765,7 @@ namespace MWMechanics // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2336,7 +1786,7 @@ namespace MWMechanics for (PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) { MWMechanics::ActiveSpells& spells = iter->first.getClass().getCreatureStats(iter->first).getActiveSpells(); - spells.purge(casterActorId); + spells.purge(iter->first, casterActorId); } } @@ -2354,7 +1804,7 @@ namespace MWMechanics { if (iter->first.getClass().getCreatureStats(iter->first).isDead()) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + adjustMagicEffects (iter->first, duration); continue; } @@ -2365,15 +1815,11 @@ namespace MWMechanics (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange) continue; - adjustMagicEffects (iter->first); + adjustMagicEffects (iter->first, duration); if (iter->first.getClass().getCreatureStats(iter->first).needToRecalcDynamicStats()) calculateDynamicStats (iter->first); calculateCreatureStatModifiers (iter->first, duration); - if (iter->first.getClass().isNpc()) - calculateNpcStatModifiers(iter->first, duration); - - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) @@ -2762,10 +2208,8 @@ namespace MWMechanics void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) { - adjustMagicEffects(ptr); + adjustMagicEffects(ptr, 0.f); calculateCreatureStatModifiers(ptr, 0.f); - if (ptr.getClass().isNpc()) - calculateNpcStatModifiers(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 171b45e270..9950a591ab 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -41,15 +41,11 @@ namespace MWMechanics { std::map mDeathCount; - void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); - - void adjustMagicEffects (const MWWorld::Ptr& creature); + void adjustMagicEffects (const MWWorld::Ptr& creature, float duration); void calculateDynamicStats (const MWWorld::Ptr& ptr); void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration); - void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration); void calculateRestoration (const MWWorld::Ptr& ptr, float duration); @@ -94,7 +90,7 @@ namespace MWMechanics /// /// \note Dead actors are ignored. - void removeActor (const MWWorld::Ptr& ptr); + void removeActor (const MWWorld::Ptr& ptr, bool keepActive); ///< Deregister an actor for stats management /// /// \note Ignored, if \a ptr is not a registered actor. @@ -208,7 +204,6 @@ namespace MWMechanics private: void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl); - void applyCureEffects (const MWWorld::Ptr& actor); PtrActorMap mActors; float mTimerDisposeSummonsCorpses; diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index f6123c12c0..eb139c918d 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -269,7 +269,8 @@ namespace MWMechanics const auto navigatorFlags = getNavigatorFlags(actor); const auto areaCosts = getAreaCosts(actor); const auto pathGridGraph = getPathGridGraph(actor.getCell()); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, halfExtents, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { @@ -281,7 +282,8 @@ namespace MWMechanics if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. - mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, halfExtents, navigatorFlags, areaCosts); + mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, halfExtents, + navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. @@ -346,7 +348,7 @@ namespace MWMechanics bool runFallback = true; - if (pathgrid && !actor.getClass().isPureWaterCreature(actor)) + if (pathgrid != nullptr && !pathgrid->mPoints.empty() && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; Misc::CoordinateConverter coords(storage.mCell->getCell()); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index c26454aab5..e6176c869c 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -208,14 +208,14 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; - bestAction.reset(new ActionSpell(it->first->mId)); - antiFleeRating = vanillaRateSpell(it->first, actor, enemy); + bestAction.reset(new ActionSpell(spell->mId)); + antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } @@ -266,9 +266,9 @@ namespace MWMechanics } } - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - float rating = rateSpell(it->first, actor, enemy); + float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 8ad9447514..b1f4355a39 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -34,6 +34,11 @@ namespace const float actorTolerance = 2 * speed * duration + 1.2 * std::max(halfExtents.x(), halfExtents.y()); return std::max(MWMechanics::MIN_TOLERANCE, actorTolerance); } + + bool canOpenDoors(const MWWorld::Ptr& ptr) + { + return ptr.getClass().isBipedal(ptr) || ptr.getClass().hasInventoryStore(ptr); + } } MWMechanics::AiPackage::AiPackage(AiPackageTypeId typeId, const Options& options) : @@ -88,7 +93,8 @@ void MWMechanics::AiPackage::reset() mObstacleCheck.clear(); } -bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance) +bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + float destTolerance, float endTolerance, PathType pathType) { const Misc::TimerStatus timerStatus = mReaction.update(duration); @@ -117,7 +123,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { - if (actor.getClass().isBipedal(actor)) + if (canOpenDoors(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; @@ -133,7 +139,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& { const auto pathfindingHalfExtents = world->getPathfindingHalfExtents(actor); mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()), - pathfindingHalfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + pathfindingHalfExtents, getNavigatorFlags(actor), getAreaCosts(actor), endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -231,7 +237,7 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (!door.isEmpty() && actor.getClass().isBipedal(actor)) + if (!door.isEmpty() && canOpenDoors(actor)) { openDoors(actor); } @@ -442,9 +448,13 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) + { result |= DetourNavigator::Flag_walk; + if (getTypeId() == AiPackageTypeId::Travel) + result |= DetourNavigator::Flag_usePathgrid; + } - if (actorClass.isBipedal(actor) && getTypeId() != AiPackageTypeId::Wander) + if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; @@ -456,20 +466,31 @@ DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts(const MWWorld::P const DetourNavigator::Flags flags = getNavigatorFlags(actor); const MWWorld::Class& actorClass = actor.getClass(); - if (flags & DetourNavigator::Flag_swim) - costs.mWater = divOrMax(costs.mWater, actorClass.getSwimSpeed(actor)); + const float swimSpeed = (flags & DetourNavigator::Flag_swim) == 0 + ? 0.0f + : actorClass.getSwimSpeed(actor); - if (flags & DetourNavigator::Flag_walk) + const float walkSpeed = [&] { - float walkCost; + if ((flags & DetourNavigator::Flag_walk) == 0) + return 0.0f; if (getTypeId() == AiPackageTypeId::Wander) - walkCost = divOrMax(1.0, actorClass.getWalkSpeed(actor)); - else - walkCost = divOrMax(1.0, actorClass.getRunSpeed(actor)); - costs.mDoor = costs.mDoor * walkCost; - costs.mPathgrid = costs.mPathgrid * walkCost; - costs.mGround = costs.mGround * walkCost; - } + return actorClass.getWalkSpeed(actor); + return actorClass.getRunSpeed(actor); + } (); + + const float maxSpeed = std::max(swimSpeed, walkSpeed); + + if (maxSpeed == 0) + return costs; + + const float swimFactor = swimSpeed / maxSpeed; + const float walkFactor = walkSpeed / maxSpeed; + + costs.mWater = divOrMax(costs.mWater, swimFactor); + costs.mDoor = divOrMax(costs.mDoor, walkFactor); + costs.mPathgrid = divOrMax(costs.mPathgrid, walkFactor); + costs.mGround = divOrMax(costs.mGround, walkFactor); return costs; } diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 6d8af0d92b..5270b74166 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -129,7 +129,8 @@ namespace MWMechanics protected: /// Handles path building and shortcutting with obstacles avoiding /** \return If the actor has arrived at his destination **/ - bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f); + bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, + float destTolerance = 0.0f, float endTolerance = 0.0f, PathType pathType = PathType::Full); /// Check if there aren't any obstacles along the path to make shortcut possible /// If a shortcut is possible then path will be cleared and filled with the destination point. diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index 05605529ad..2d896ddbdc 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -53,7 +53,7 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte const float pathTolerance = 100.f; // check the true distance in case the target is far away in Z-direction - bool reached = pathTo(actor, dest, duration, pathTolerance) && + bool reached = pathTo(actor, dest, duration, pathTolerance, (actorPos - dest).length(), PathType::Partial) && std::abs(dest.z() - actorPos.z()) < pathTolerance; if (reached) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index fada7761df..bf4cf28deb 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -361,11 +361,12 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo // insert new package in correct place depending on priority for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { - // We should keep current AiCast package, if we try to add a new one. + // We should override current AiCast package, if we try to add a new one. if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Cast && package.getTypeId() == MWMechanics::AiPackageTypeId::Cast) { - continue; + *it = package.clone(); + return; } if((*it)->getPriority() <= package.getPriority()) diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 8e5372c46d..2594adcb34 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -1,5 +1,7 @@ #include "aitravel.hpp" +#include + #include #include "../mwbase/environment.hpp" @@ -23,6 +25,11 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) return (pos1 - pos2).length2() <= 7168*7168; } + float getActorRadius(const MWWorld::ConstPtr& actor) + { + const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + return std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + } } namespace MWMechanics @@ -70,16 +77,24 @@ namespace MWMechanics // Unfortunately, with vanilla assets destination is sometimes blocked by other actor. // If we got close to target, check for actors nearby. If they are, finish AI package. - int destinationTolerance = 64; - if (distance(actorPos, targetPos) <= destinationTolerance) + if (mDestinationCheck.update(duration) == Misc::TimerStatus::Elapsed) { - std::vector targetActors; - std::pair result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors); - - if (!result.first.isEmpty()) + std::vector occupyingActors; + if (isAreaOccupiedByOtherActor(actor, targetPos, &occupyingActors)) { - actor.getClass().getMovementSettings(actor).mPosition[1] = 0; - return true; + const float actorRadius = getActorRadius(actor); + const float distanceToTarget = distance(actorPos, targetPos); + for (const MWWorld::Ptr& other : occupyingActors) + { + const float otherRadius = getActorRadius(other); + const auto [minRadius, maxRadius] = std::minmax(actorRadius, otherRadius); + constexpr float toleranceFactor = 1.25; + if (minRadius * toleranceFactor + maxRadius > distanceToTarget) + { + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + return true; + } + } } } @@ -93,11 +108,12 @@ namespace MWMechanics void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) { - if (!isWithinMaxRange(osg::Vec3f(mX, mY, mZ), actor.getRefData().getPosition().asVec3())) + osg::Vec3f pos(mX, mY, mZ); + if (!isWithinMaxRange(pos, actor.getRefData().getPosition().asVec3())) return; // does not do any validation on the travel target (whether it's in air, inside collision geometry, etc), // that is the user's responsibility - MWBase::Environment::get().getWorld()->moveObject(actor, mX, mY, mZ); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); reset(); } diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 2ea2a8f717..ee4ff9ad4d 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -52,6 +52,8 @@ namespace MWMechanics const float mZ; const bool mHidden; + + AiReactionTimer mDestinationCheck; }; struct AiInternalTravel final : public AiTravel diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 0e424b2f8b..9c84555404 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -85,14 +85,6 @@ namespace MWMechanics return MWBase::Environment::get().getWorld()->castRay(position, visibleDestination, mask, actor); } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr &actor, const osg::Vec3f& destination) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor); - } - void stopMovement(const MWWorld::Ptr& actor) { actor.getClass().getMovementSettings(actor).mPosition[0] = 0; @@ -201,8 +193,10 @@ namespace MWMechanics else { const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor); + constexpr float endTolerance = 0; mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), - getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor)); + getPathGridGraph(actor.getCell()), halfExtents, getNavigatorFlags(actor), getAreaCosts(actor), + endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) @@ -361,11 +355,13 @@ namespace MWMechanics if (isAreaOccupiedByOtherActor(actor, mDestination)) continue; + constexpr float endTolerance = 0; + if (isWaterCreature || isFlyingCreature) mPathFinder.buildStraightPath(mDestination); else mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags, - areaCosts); + areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) { @@ -744,8 +740,8 @@ namespace MWMechanics state.moveIn(new AiWanderStorage()); - MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), - static_cast(dest.mY), static_cast(dest.mZ)); + osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); + MWBase::Environment::get().getWorld()->moveObject(actor, pos); actor.getClass().adjustPosition(actor, false); } @@ -754,6 +750,9 @@ namespace MWMechanics const ESM::Pathgrid *pathgrid = MWBase::Environment::get().getWorld()->getStore().get().search(*currentCell->getCell()); + if (pathgrid == nullptr || pathgrid->mPoints.empty()) + return; + int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(currentCell).getNeighbouringPoints(index, points); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index d5a1d46a88..ed26e2b9a4 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -23,6 +23,7 @@ #include #include +#include #include @@ -942,14 +943,6 @@ CharacterController::~CharacterController() } } -void split(const std::string &s, char delim, std::vector &elems) { - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, delim)) { - elems.push_back(item); - } -} - void CharacterController::handleTextKey(const std::string &groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { const std::string &evt = key->second; @@ -969,7 +962,7 @@ void CharacterController::handleTextKey(const std::string &groupname, SceneUtil: if (soundgen.find(' ') != std::string::npos) { std::vector tokens; - split(soundgen, ' ', tokens); + Misc::StringUtils::split(soundgen, tokens); soundgen = tokens[0]; if (tokens.size() >= 2) { @@ -989,8 +982,7 @@ void CharacterController::handleTextKey(const std::string &groupname, SceneUtil: if(!sound.empty()) { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - // NB: landing sound is not played for NPCs here - if(soundgen == "left" || soundgen == "right" || soundgen == "land") + if (soundgen == "left" || soundgen == "right") { sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal); @@ -1661,7 +1653,8 @@ bool CharacterController::updateWeaponState(CharacterState& idle) MWRender::Animation::BlendMask_All, false, weapSpeed, startKey, stopKey, 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; + if(mAnimation->getCurrentTime(mCurrentWeapon) != -1.f) + mUpperBodyState = UpperCharState_StartToMinAttack; } } @@ -2394,12 +2387,15 @@ void CharacterController::update(float duration) if(!isKnockedDown() && !isKnockedOut()) { if (rot != osg::Vec3f()) - world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); + world->rotateObject(mPtr, rot, true); } else //avoid z-rotating for knockdown { if (rot.x() != 0 && rot.y() != 0) - world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); + { + rot.z() = 0.0f; + world->rotateObject(mPtr, rot, true); + } } if (!mMovementAnimationControlled) diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 1ff44fcbb0..8ca6e8b766 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -1,6 +1,7 @@ #include "creaturestats.hpp" #include +#include #include #include @@ -544,40 +545,37 @@ namespace MWMechanics mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); - state.mSummonedCreatureMap = mSummonedCreatures; + state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; for (int i=0; i<4; ++i) mAiSettings[i].writeState (state.mAiSettings[i]); - - for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - for (int i=0; ifirst].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i]; - - state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm(); - } } void CreatureStats::readState (const ESM::CreatureStats& state) { - for (int i=0; ifirst].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i]; - - mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - } } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) @@ -704,7 +693,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -713,23 +702,4 @@ namespace MWMechanics { return mSummonGraveyard; } - - std::map &CreatureStats::getCorprusSpells() - { - return mCorprusSpells; - } - - void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats) - { - mCorprusSpells[sourceId] = stats; - } - - void CreatureStats::removeCorprusSpell(const std::string& sourceId) - { - auto corprusIt = mCorprusSpells.find(sourceId); - if (corprusIt != mCorprusSpells.end()) - { - mCorprusSpells.erase(corprusIt); - } - } } diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index e09f5197e1..d234d14486 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H +#include #include #include #include @@ -70,6 +71,8 @@ namespace MWMechanics MWWorld::TimeStamp mLastRestock; // The pool of merchant gold (not in inventory) + // HACK: value of INT_MIN has a special meaning: indicates a converted .ess file + // (this is a workaround to avoid changing the save file format) int mGoldPool; int mActorId; @@ -85,14 +88,12 @@ namespace MWMechanics float mSideMovementAngle; private: - std::map mSummonedCreatures; // + std::multimap mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. std::vector mSummonGraveyard; - std::map mCorprusSpells; - protected: int mLevel; @@ -234,7 +235,7 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag @@ -295,12 +296,6 @@ namespace MWMechanics static void cleanup(); - std::map & getCorprusSpells(); - - void addCorprusSpell(const std::string& sourceId, CorprusStats& stats); - - void removeCorprusSpell(const std::string& sourceId); - float getSideMovementAngle() const { return mSideMovementAngle; } void setSideMovementAngle(float angle) { mSideMovementAngle = angle; } }; diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 5d4bfb682f..426a66ecf2 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -30,12 +30,11 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getStore().get().find( "fDiseaseXferChance")->mValue.getFloat(); - MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); + const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); - for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it) + for (const ESM::Spell* spell : spells) { - const ESM::Spell* spell = it->first; if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; @@ -56,7 +55,7 @@ namespace MWMechanics if (Misc::Rng::rollDice(10000) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(it->first); + actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); std::string msg = "sMagicContractDisease"; diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp deleted file mode 100644 index b0defac7d2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "linkedeffects.hpp" - -#include -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" - -#include "../mwrender/animation.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/esmstore.hpp" - -#include "creaturestats.hpp" - -namespace MWMechanics -{ - - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects) - { - if (caster.isEmpty() || caster == target || !target.getClass().isActor()) - return false; - - bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; - bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable; - if (!isHarmful || isUnreflectable) - return false; - - float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude(); - if (Misc::Rng::roll0to99() >= reflect) - return false; - - const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Reflect"); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation && !reflectStatic->mModel.empty()) - animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string()); - reflectedEffects.mList.emplace_back(effect); - return true; - } - - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source) - { - if (caster.isEmpty() || caster == target) - return; - - if (!target.getClass().isActor() || !caster.getClass().isActor()) - return; - - // Make sure callers don't do something weird - if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill) - throw std::runtime_error("invalid absorb stat effect"); - - if (appliedEffect.mMagnitude == 0) - return; - - std::vector absorbEffects; - ActiveSpells::ActiveEffect absorbEffect = appliedEffect; - absorbEffect.mMagnitude *= -1; - absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; - absorbEffects.emplace_back(absorbEffect); - - // Morrowind negates reflected Absorb spells so the original caster won't be harmed. - if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) - { - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId()); - return; - } - - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true, - absorbEffects, source, target.getClass().getCreatureStats(target).getActorId()); - } -} diff --git a/apps/openmw/mwmechanics/linkedeffects.hpp b/apps/openmw/mwmechanics/linkedeffects.hpp deleted file mode 100644 index a6dea2a3a2..0000000000 --- a/apps/openmw/mwmechanics/linkedeffects.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef MWMECHANICS_LINKEDEFFECTS_H -#define MWMECHANICS_LINKEDEFFECTS_H - -#include - -namespace ESM -{ - struct ActiveEffect; - struct EffectList; - struct ENAMstruct; - struct MagicEffect; - struct Spell; -} - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - - // Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list. - bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects); - - // Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster. - void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect, - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source); -} - -#endif diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index 1a1a44f638..e75361628b 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -193,22 +193,22 @@ namespace MWMechanics void MagicEffects::writeState(ESM::MagicEffects &state) const { - // Don't need to save Modifiers, they are recalculated every frame anyway. - for (Collection::const_iterator iter (begin()); iter!=end(); ++iter) + for (const auto& [key, params] : mCollection) { - if (iter->second.getBase() != 0) + if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions - state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase())); + state.mEffects[key.mId] = {params.getBase(), params.getModifier()}; } } } void MagicEffects::readState(const ESM::MagicEffects &state) { - for (std::map::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it) + for (const auto& [key, params] : state.mEffects) { - mCollection[EffectKey(it->first)].setBase(it->second); + mCollection[EffectKey(key)].setBase(params.first); + mCollection[EffectKey(key)].setModifier(params.second); } } } diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 12735a87fc..50f4dab050 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -69,16 +69,6 @@ namespace MWMechanics return param -= right; } - // Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display - struct EffectSourceVisitor - { - virtual ~EffectSourceVisitor() { } - - virtual void visit (EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) = 0; - }; - /// \brief Effects currently affecting a NPC or creature class MagicEffects { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index e790f21413..5e18e737df 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -84,7 +84,7 @@ namespace MWMechanics // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); - creatureStats.modifyMagicEffects(MagicEffects()); + creatureStats.getActiveSpells().clear(ptr); for (int i=0; i<27; ++i) npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]); @@ -213,6 +213,7 @@ namespace MWMechanics int attributes[ESM::Attribute::Length]; for (int i=0; i selectedSpells = autoCalcPlayerSpells(skills, attributes, race); @@ -221,6 +222,7 @@ namespace MWMechanics // forced update and current value adjustments mActors.updateActor (ptr, 0); + mActors.updateActor (ptr, 0); for (int i=0; i<3; ++i) { @@ -257,11 +259,11 @@ namespace MWMechanics mActors.castSpell(ptr, spellId, manualSpell); } - void MechanicsManager::remove(const MWWorld::Ptr& ptr) + void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) { if(ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); - mActors.removeActor(ptr); + mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } @@ -282,24 +284,6 @@ namespace MWMechanics mObjects.dropObjects(cellStore); } - void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) - { - auto& stats = actor.getClass().getCreatureStats (actor); - auto& corprusSpells = stats.getCorprusSpells(); - - auto corprusIt = corprusSpells.find(sourceId); - - if (corprusIt != corprusSpells.end()) - { - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - MWMechanics::AttributeValue attr = stats.getAttribute(i); - attr.restore(corprusIt->second.mWorsenings[i]); - actor.getClass().getCreatureStats(actor).setAttribute(i, attr); - } - } - } - void MechanicsManager::update(float duration, bool paused) { // Note: we should do it here since game mechanics and world updates use these values @@ -333,7 +317,7 @@ namespace MWMechanics // HACK? The player has been changed, so a new Animation object may // have been made for them. Make sure they're properly updated. - mActors.removeActor(ptr); + mActors.removeActor(ptr, true); mActors.addActor(ptr, true); } @@ -483,7 +467,7 @@ namespace MWMechanics mUpdatePlayer = true; } - int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange) + int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { const MWMechanics::NpcStats& npcSkill = ptr.getClass().getNpcStats(ptr); float x = static_cast(npcSkill.getBaseDisposition()); @@ -562,11 +546,9 @@ namespace MWMechanics x += ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Charm).getMagnitude(); - if(addTemporaryDispositionChange) - x += MWBase::Environment::get().getDialogueManager()->getTemporaryDispositionChange(); - - int effective_disposition = std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used - return effective_disposition; + if(clamp) + return std::max(0,std::min(int(x),100));//, normally clamped to [0..100] when used + return int(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) @@ -603,7 +585,7 @@ namespace MWMechanics return mActors.countDeaths (id); } - void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) + void MechanicsManager::getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) { const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -727,19 +709,22 @@ namespace MWMechanics x = success ? std::max(iPerMinChange, c) : c; } - tempChange = type == PT_Intimidate ? x : int(x * fPerTempMult); + tempChange = type == PT_Intimidate ? int(x) : int(x * fPerTempMult); - float cappedDispositionChange = tempChange; - if (currentDisposition + tempChange > 100.f) - cappedDispositionChange = static_cast(100 - currentDisposition); - if (currentDisposition + tempChange < 0.f) - cappedDispositionChange = static_cast(-currentDisposition); + int cappedDispositionChange = tempChange; + if (currentDisposition + tempChange > 100) + cappedDispositionChange = 100 - currentDisposition; + if (currentDisposition + tempChange < 0) + { + cappedDispositionChange = -currentDisposition; + tempChange = 0; + } permChange = floor(cappedDispositionChange / fPerTempMult); if (type == PT_Intimidate) { - permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : y; + permChange = success ? -int(cappedDispositionChange/ fPerTempMult) : int(y); } } @@ -1722,7 +1707,7 @@ namespace MWMechanics int disposition = 50; if (ptr.getClass().isNpc()) - disposition = getDerivedDisposition(ptr, true); + disposition = getDerivedDisposition(ptr); int fight = ptr.getClass().getCreatureStats(ptr).getAiSetting(CreatureStats::AI_Fight).getModified() + static_cast(getFightDistanceBias(ptr, target) + getFightDispositionBias(static_cast(disposition))); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index ed28c0a463..06da2fde51 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -45,7 +45,7 @@ namespace MWMechanics void add (const MWWorld::Ptr& ptr) override; ///< Register an object for management - void remove (const MWWorld::Ptr& ptr) override; + void remove (const MWWorld::Ptr& ptr, bool keepActive) override; ///< Deregister an object for management void updateCell(const MWWorld::Ptr &old, const MWWorld::Ptr &ptr) override; @@ -87,13 +87,13 @@ namespace MWMechanics int getBarterOffer(const MWWorld::Ptr& ptr,int basePrice, bool buying) override; ///< This is used by every service to determine the price of objects given the trading skills of the player and NPC. - int getDerivedDisposition(const MWWorld::Ptr& ptr, bool addTemporaryDispositionChange = true) override; + int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) override; ///< Calculate the diposition of an NPC toward the player. int countDeaths (const std::string& id) const override; ///< Return the number of deaths for actors with the given ID. - void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, float& tempChange, float& permChange) override; + void getPersuasionDispositionChange (const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) override; ///< Perform a persuasion action on NPC /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! @@ -230,8 +230,6 @@ namespace MWMechanics GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override; bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override; - void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override; - private: bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set &playerFollowers); diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 88325ee7c7..344cc82058 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -4,6 +4,8 @@ #include "../mwworld/class.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" #include "movement.hpp" @@ -72,6 +74,15 @@ namespace MWMechanics return MWWorld::Ptr(); // none found } + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, + std::vector* occupyingActors) + { + const auto world = MWBase::Environment::get().getWorld(); + const osg::Vec3f halfExtents = world->getPathfindingHalfExtents(actor); + const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, actor, occupyingActors); + } + ObstacleCheck::ObstacleCheck() : mWalkState(WalkState::Initial) , mStateDuration(0) diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index b574bab67f..24bd5ed1c1 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -3,9 +3,12 @@ #include +#include + namespace MWWorld { class Ptr; + class ConstPtr; } namespace MWMechanics @@ -21,6 +24,9 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, + std::vector* occupyingActors = nullptr); + class ObstacleCheck { public: diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 2f8e830434..4b06993a49 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -206,9 +206,6 @@ namespace MWMechanics endPointInLocalCoords, startNode); - if (!endNode.second) - return; - // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); @@ -279,7 +276,8 @@ namespace MWMechanics // unreachable pathgrid point. // // The AI routines will have to deal with such situations. - *out++ = endPoint; + if (endNode.second) + *out++ = endPoint; } float PathFinder::getZAngleToNext(float x, float y) const @@ -364,13 +362,13 @@ namespace MWMechanics void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, - areaCosts, std::back_inserter(mPath)); + areaCosts, endTolerance, pathType, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -383,7 +381,8 @@ namespace MWMechanics void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { mPath.clear(); mCell = cell; @@ -392,15 +391,16 @@ namespace MWMechanics if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { - status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)); + status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, + endTolerance, pathType, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } - if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty()) + if (status != DetourNavigator::Status::NavMeshNotFound && mPath.empty() && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, std::back_inserter(mPath)); + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } @@ -416,12 +416,17 @@ namespace MWMechanics DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, std::back_insert_iterator> out) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::back_insert_iterator> out) { const auto world = MWBase::Environment::get().getWorld(); const auto stepSize = getPathStepSize(actor); const auto navigator = world->getNavigator(); - const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts, out); + const auto status = navigator->findPath(halfExtents, stepSize, startPoint, endPoint, flags, areaCosts, + endTolerance, out); + + if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) + return DetourNavigator::Status::Success; if (status != DetourNavigator::Status::Success) { @@ -449,8 +454,9 @@ namespace MWMechanics const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); 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, - prePathInserter); + endTolerance, prePathInserter); if (status == DetourNavigator::Status::NavMeshNotFound) return; @@ -475,7 +481,8 @@ namespace MWMechanics void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts) + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance = std::min( @@ -485,8 +492,9 @@ namespace MWMechanics const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, halfExtents, flags, areaCosts); + return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, halfExtents, flags, areaCosts, + endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, halfExtents, flags, areaCosts); + buildPath(actor, startPoint, end, cell, pathgridGraph, halfExtents, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 0ada08d3fb..f0a5040334 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -70,6 +70,12 @@ namespace MWMechanics // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); + enum class PathType + { + Full, + Partial, + }; + class PathFinder { public: @@ -93,18 +99,20 @@ namespace MWMechanics void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts); + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); void buildPathByNavMeshToNextPoint(const MWWorld::ConstPtr& actor, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts); + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, @@ -212,7 +220,7 @@ namespace MWMechanics [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, - const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, std::back_insert_iterator> out); }; } diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp index cb80921b33..82531132ca 100644 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ b/apps/openmw/mwmechanics/spellabsorption.cpp @@ -16,33 +16,30 @@ namespace MWMechanics { - - class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor + float getProbability(const MWMechanics::ActiveSpells& activeSpells) { - public: - float mProbability{0.f}; - - GetAbsorptionProbability() = default; - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float magnitude, float /*remainingTime*/, float /*totalTime*/) override + float probability = 0.f; + for(const auto& params : activeSpells) { - if (key.mId == ESM::MagicEffect::SpellAbsorption) + for(const auto& effect : params.getEffects()) { - if (mProbability == 0.f) - mProbability = magnitude / 100; - else + if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption) { - // If there are different sources of SpellAbsorption effect, multiply failing probability for all effects. - // Real absorption probability will be the (1 - total fail chance) in this case. - float failProbability = 1.f - mProbability; - failProbability *= 1.f - magnitude / 100; - mProbability = 1.f - failProbability; + 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) { @@ -53,13 +50,7 @@ namespace MWMechanics if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f) return false; - GetAbsorptionProbability check; - stats.getActiveSpells().visitEffectSources(check); - stats.getSpells().visitEffectSources(check); - if (target.getClass().hasInventoryStore(target)) - target.getClass().getInventoryStore(target).visitEffectSources(check); - - int chance = check.mProbability * 100; + int chance = getProbability(stats.getActiveSpells()); if (Misc::Rng::roll0to99() >= chance) return false; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 6b07fdbb86..53fbe69ab3 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -22,14 +22,38 @@ #include "actorutil.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" -#include "linkedeffects.hpp" #include "spellabsorption.hpp" -#include "spellresistance.hpp" +#include "spelleffects.hpp" #include "spellutil.hpp" #include "summoning.hpp" -#include "tickableeffects.hpp" #include "weapontype.hpp" +namespace +{ + 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) @@ -54,7 +78,7 @@ namespace MWMechanics (mTarget.getRefData().getPosition().asVec3() + offset) - (mCaster.getRefData().getPosition().asVec3()); - MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); + MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mSlot); } void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, @@ -100,20 +124,13 @@ namespace MWMechanics } ESM::EffectList reflectedEffects; - std::vector appliedLastingEffects; - - // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. - // This is required for Weakness effects in a spell to apply to any subsequent effects in the spell. - // Otherwise, they'd only apply after the whole spell was added. - MagicEffects targetEffects; - if (targetIsActor) - targetEffects += target.getClass().getCreatureStats(target).getMagicEffects(); + ActiveSpells::ActiveSpellParams params(*this, caster); bool castByPlayer = (!caster.isEmpty() && caster == getPlayer()); - ActiveSpells targetSpells; + const ActiveSpells* targetSpells = nullptr; if (targetIsActor) - targetSpells = target.getClass().getCreatureStats(target).getActiveSpells(); + targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); bool canCastAnEffect = false; // For bound equipment.If this remains false // throughout the iteration of this spell's @@ -134,7 +151,7 @@ namespace MWMechanics effectIt->mEffectID); // Re-casting a bound equipment effect has no effect if the spell is still active - if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId)) + if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells && targetSpells->isSpellActive(mId)) { if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); @@ -163,288 +180,70 @@ namespace MWMechanics if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects)) continue; - // Try resisting. - float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects); - if (magnitudeMult == 0) + ActiveSpells::ActiveEffect effect; + effect.mEffectId = effectIt->mEffectID; + effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = effectIt->mMagnMin; + effect.mMaxMagnitude = effectIt->mMagnMax; + effect.mTimeLeft = 0.f; + effect.mEffectIndex = currentEffectIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + + // Avoid applying harmful effects to the player in god mode + if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) { - // Fully resisted, show message - if (target == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}"); - else if (castByPlayer) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); + effect.mMinMagnitude = 0; + effect.mMaxMagnitude = 0; } - else + + bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; + + bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + + effect.mTimeLeft = effect.mDuration; + + // add to list of active effects, to apply in next frame + params.getEffects().emplace_back(effect); + + bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth) { - float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1); - magnitude *= magnitudeMult; + // 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 (!target.getClass().isActor()) - { - // non-actor objects have no list of active magic effects, so have to apply instantly - if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude)) - continue; - } - else // target.getClass().isActor() == true - { - ActiveSpells::ActiveEffect effect; - effect.mEffectId = effectIt->mEffectID; - effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; - effect.mMagnitude = magnitude; - effect.mTimeLeft = 0.f; - effect.mEffectIndex = currentEffectIndex; - - // Avoid applying absorb effects if the caster is the target - // We still need the spell to be added - if (caster == target - && effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute - && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - { - effect.mMagnitude = 0; - } - - // Avoid applying harmful effects to the player in god mode - if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful) - { - effect.mMagnitude = 0; - } - - bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth) - { - // If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar. - MWBase::Environment::get().getWindowManager()->setEnemy(target); - } - - bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(effectIt->mDuration) : 1.f; - - bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); - - if (effect.mDuration == 0) - { - // We still should add effect to list to allow GetSpellEffects to detect this spell - appliedLastingEffects.push_back(effect); - - // duration 0 means apply full magnitude instantly - bool wasDead = target.getClass().getCreatureStats(target).isDead(); - effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude); - bool isDead = target.getClass().getCreatureStats(target).isDead(); - - if (!wasDead && isDead) - MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster); - } - else - { - effect.mTimeLeft = effect.mDuration; - - targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude)); - - // add to list of active effects, to apply in next frame - appliedLastingEffects.push_back(effect); - - // Unequip all items, if a spell with the ExtraSpell effect was casted - if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target)) - { - MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target); - store.unequipAll(target); - } - - // Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target - if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc()) - || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) - && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) - { - MWMechanics::AiFollow package(caster, true); - target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); - } - - // For absorb effects, also apply the effect to the caster - but with a negative - // magnitude, since we're transferring stats from the target to the caster - if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill) - absorbStat(*effectIt, effect, caster, target, reflected, mSourceName); - } - } - - // Re-casting a summon effect will remove the creature from previous castings of that effect. - if (isSummoningEffect(effectIt->mEffectID) && targetIsActor) - { - CreatureStats& targetStats = target.getClass().getCreatureStats(target); - ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); - auto findCreature = targetStats.getSummonedCreatureMap().find(key); - if (findCreature != targetStats.getSummonedCreatureMap().end()) - { - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second); - targetStats.getSummonedCreatureMap().erase(findCreature); - } - } - - if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - - // Add VFX - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - else - castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); - - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Note: in case of non actor, a free effect should be fine as well - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } + if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + { + playEffects(target, *magicEffect); } } if (!exploded) - MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile); + MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile, mSlot); if (!target.isEmpty()) { if (!reflectedEffects.mList.empty()) inflict(caster, target, reflectedEffects, range, true, exploded); - if (!appliedLastingEffects.empty()) + if (!params.getEffects().empty()) { - int casterActorId = -1; - if (!caster.isEmpty() && caster.getClass().isActor()) - casterActorId = caster.getClass().getCreatureStats(caster).getActorId(); - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects, - mSourceName, casterActorId); - } - } - } - - bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) - { - short effectId = effect.mId; - if (target.getClass().canLock(target)) - { - if (effectId == ESM::MagicEffect::Lock) - { - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); - target.getCellRef().lock(static_cast(magnitude)); - } - return true; - } - else if (effectId == ESM::MagicEffect::Open) - { - if (!caster.isEmpty()) - { - MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); - // Use the player instead of the caster for vanilla crime compatibility - } - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - if (animation) - animation->addSpellCastGlow(magiceffect); - if (target.getCellRef().getLockLevel() <= magnitude) - { - if (target.getCellRef().getLockLevel() > 0) - { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); - } - target.getCellRef().unlock(); - } + if(targetIsActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); else { - MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + // Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway + for(auto& effect : params.getEffects()) + applyMagicEffect(target, caster, params, effect, 0.f); } - - return true; } } - else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel) - { - target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true); - return true; - } - else if (target.getClass().isActor() && target == getPlayer()) - { - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster); - bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled(); - - if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; - MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker); - anim->removeEffect(effectId); - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_end"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1); - return true; - } - else if (effectId == ESM::MagicEffect::Mark) - { - if (teleportingEnabled) - { - MWBase::Environment::get().getWorld()->getPlayer().markPosition( - target.getCell(), target.getRefData().getPosition()); - } - else if (caster == getPlayer()) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - } - return true; - } - else if (effectId == ESM::MagicEffect::Recall) - { - if (!teleportingEnabled) - { - if (caster == getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); - return true; - } - - MWWorld::CellStore* markedCell = nullptr; - ESM::Position markedPosition; - - MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); - if (markedCell) - { - MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, - markedPosition, false); - action.execute(target); - anim->removeEffect(effectId); - } - return true; - } - } - return false; } - bool CastSpell::cast(const std::string &id) { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -460,7 +259,7 @@ namespace MWMechanics throw std::runtime_error("ID type cannot be casted"); } - bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile) + bool CastSpell::cast(const MWWorld::Ptr &item, int slot, bool launchProjectile) { std::string enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) @@ -471,7 +270,7 @@ namespace MWMechanics const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find(enchantmentName); - mStack = false; + mSlot = slot; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); bool isProjectile = false; @@ -551,7 +350,7 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self); @@ -562,7 +361,6 @@ namespace MWMechanics { mSourceName = spell->mName; mId = spell->mId; - mStack = false; const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); @@ -637,7 +435,7 @@ namespace MWMechanics bool CastSpell::cast (const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mStack = true; + mType = ESM::ActiveSpells::Type_Consumable; mSourceName = ingredient->mName; ESM::ENAMstruct effect; @@ -780,4 +578,36 @@ namespace MWMechanics sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); } } + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping) + { + if (playNonLooping) + { + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(!magicEffect.mHitSound.empty()) + sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f); + else + sndMgr->playSound3D(target, schools[magicEffect.mData.mSchool]+" hit", 1.0f, 1.0f); + } + + // Add VFX + const ESM::Static* castStatic; + if (!magicEffect.mHit.empty()) + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect.mHit); + else + castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_DefaultHit"); + + bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target); + if(anim && !castStatic->mModel.empty()) + { + // Don't play particle VFX unless the effect is new or it should be looping. + if (playNonLooping || loop) + anim->addEffect("meshes\\" + castStatic->mModel, magicEffect.mIndex, loop, "", magicEffect.mParticle); + } + } } diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index cc525d2db6..a21ea33e0b 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -1,6 +1,7 @@ #ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H +#include #include #include "../mwworld/ptr.hpp" @@ -11,6 +12,7 @@ namespace ESM struct Ingredient; struct Potion; struct EffectList; + struct MagicEffect; } namespace MWMechanics @@ -26,13 +28,14 @@ namespace MWMechanics void playSpellCastingEffects(const std::vector& effects); public: - bool mStack{false}; std::string mId; // ID of spell, potion, item etc std::string mSourceName; // Display name for spell, potion, etc osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) + int mSlot{0}; + ESM::ActiveSpells::EffectType mType{ESM::ActiveSpells::Type_Temporary}; public: CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false); @@ -41,7 +44,7 @@ namespace MWMechanics /// @note mCaster must be an actor /// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster. - bool cast (const MWWorld::Ptr& item, bool launchProjectile=true); + bool cast (const MWWorld::Ptr& item, int slot, bool launchProjectile=true); /// @note mCaster must be an NPC bool cast (const ESM::Ingredient* ingredient); @@ -60,11 +63,9 @@ namespace MWMechanics /// @note \a caster can be any type of object, or even an empty object. void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false); - - /// @note \a caster can be any type of object, or even an empty object. - /// @return was the target suitable for the effect? - bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude); }; + + void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true); } #endif diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp new file mode 100644 index 0000000000..24816b9ed1 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -0,0 +1,1039 @@ +#include "spelleffects.hpp" + +#include + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/aifollow.hpp" +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/spellresistance.hpp" +#include "../mwmechanics/summoning.hpp" + +#include "../mwrender/animation.hpp" + +#include "../mwworld/actionequip.hpp" +#include "../mwworld/actionteleport.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace +{ + float roll(const ESM::ActiveEffect& effect) + { + if(effect.mMinMagnitude == effect.mMaxMagnitude) + return effect.mMinMagnitude; + return effect.mMinMagnitude + Misc::Rng::rollDice(effect.mMaxMagnitude - effect.mMinMagnitude + 1); + } + + void modifyAiSetting(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, ESM::MagicEffect::Effects creatureEffect, MWMechanics::CreatureStats::AiSetting setting, float magnitude, bool& invalid) + { + if(target == MWMechanics::getPlayer() || (effect.mEffectId == creatureEffect) == target.getClass().isNpc()) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getAiSetting(setting); + stat.setModifier(static_cast(stat.getModifier() - magnitude)); + creatureStats.setAiSetting(setting, stat); + } + } + + void adjustDynamicStat(const MWWorld::Ptr& target, int index, float magnitude, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = false) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero, allowIncreaseAboveModified); + creatureStats.setDynamic(index, stat); + } + + void modDynamicStat(const MWWorld::Ptr& target, int index, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto stat = creatureStats.getDynamic(index); + float current = stat.getCurrent(); + stat.setModified(stat.getModified() - magnitude, 0); + stat.setCurrentModified(stat.getCurrentModified() - magnitude); + stat.setCurrent(current - magnitude); + creatureStats.setDynamic(index, stat); + } + + void damageAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.damage(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void restoreAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.restore(magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void fortifyAttribute(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + auto attr = creatureStats.getAttribute(effect.mArg); + attr.setModifier(attr.getModifier() + magnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + + void damageSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.damage(magnitude); + } + + void restoreSkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.restore(magnitude); + } + + void fortifySkill(const MWWorld::Ptr& target, const ESM::ActiveEffect& effect, float magnitude) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setModifier(skill.getModifier() + magnitude); + } + + bool disintegrateSlot(const MWWorld::Ptr& ptr, int slot, float disintegrate) + { + MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); + MWWorld::ContainerStoreIterator item = inv.getSlot(slot); + + if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) + { + if (!item->getClass().hasItemHealth(*item)) + return false; + int charge = item->getClass().getItemHealth(*item); + if (charge == 0) + return false; + + // Store remainder of disintegrate amount (automatically subtracted if > 1) + item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); + + charge = item->getClass().getItemHealth(*item); + charge -= std::min(static_cast(disintegrate), charge); + item->getCellRef().setCharge(charge); + + if (charge == 0) + { + // Will unequip the broken item and try to find a replacement + if (ptr != MWMechanics::getPlayer()) + inv.autoEquip(ptr); + else + inv.unequipItem(*item, ptr); + } + + return true; + } + + return false; + } + + int getBoundItemSlot(const MWWorld::Ptr& boundPtr) + { + const auto [slots, _] = boundPtr.getClass().getEquipmentSlots(boundPtr); + if(!slots.empty()) + return slots[0]; + return -1; + } + + void addBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + + int slot = getBoundItemSlot(boundPtr); + auto prevItem = slot >= 0 ? store.getSlot(slot) : store.end(); + + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem; + auto it = slot >= 0 ? store.getSlot(slot) : store.end(); + // Equip can fail because beast races cannot equip boots/helmets + if(it != store.end()) + newItem = *it; + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState_Weapon); + + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + void removeBoundItem(const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + auto item = std::find_if(store.begin(), store.end(), [&] (const auto& it) + { + return Misc::StringUtils::ciEqual(it.getCellRef().getRefId(), itemId); + }); + if(item == store.end()) + return; + int slot = getBoundItemSlot(*item); + + auto currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); + + if (actor != MWMechanics::getPlayer()) + { + store.remove(itemId, 1, actor); + + // Equip a replacement + if (!wasEquipped) + return; + + std::string type = currentItem->getTypeName(); + if (type != typeid(ESM::Weapon).name() && type != typeid(ESM::Armor).name() && type != typeid(ESM::Clothing).name()) + return; + + if (actor.getClass().getCreatureStats(actor).isDead()) + return; + + if (!actor.getClass().hasInventoryStore(actor)) + return; + + if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) + return; + + actor.getClass().getInventoryStore(actor).autoEquip(actor); + + return; + } + + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); + + if (!prevItemId.empty() && wasEquipped) + { + // Find previous item (or its replacement) by id. + // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr prevItem = store.findReplacement(prevItemId); + if (!prevItem.isEmpty()) + { + MWWorld::ActionEquip action(prevItem); + action.execute(actor); + } + } + + store.remove(itemId, 1, actor); + } + + bool isCorprusEffect(const MWMechanics::ActiveSpells::ActiveEffect& effect, bool harmfulOnly = false) + { + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId != ESM::MagicEffect::Corprus) + { + const auto* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce && (!harmfulOnly || magicEffect->mData.mFlags & ESM::MagicEffect::Flags::Harmful)) + return true; + } + return false; + } + + static const std::map sBoundItemsMap{ + {ESM::MagicEffect::BoundBattleAxe, "sMagicBoundBattleAxeID"}, + {ESM::MagicEffect::BoundBoots, "sMagicBoundBootsID"}, + {ESM::MagicEffect::BoundCuirass, "sMagicBoundCuirassID"}, + {ESM::MagicEffect::BoundDagger, "sMagicBoundDaggerID"}, + {ESM::MagicEffect::BoundGloves, "sMagicBoundLeftGauntletID"}, + {ESM::MagicEffect::BoundHelm, "sMagicBoundHelmID"}, + {ESM::MagicEffect::BoundLongbow, "sMagicBoundLongbowID"}, + {ESM::MagicEffect::BoundLongsword, "sMagicBoundLongswordID"}, + {ESM::MagicEffect::BoundMace, "sMagicBoundMaceID"}, + {ESM::MagicEffect::BoundShield, "sMagicBoundShieldID"}, + {ESM::MagicEffect::BoundSpear, "sMagicBoundSpearID"} + }; +} + +namespace MWMechanics +{ + +void applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, bool& invalid, bool& receivedMagicDamage) +{ + const auto world = MWBase::Environment::get().getWorld(); + bool godmode = target == getPlayer() && world->getGodModeState(); + switch(effect.mEffectId) + { + case ESM::MagicEffect::CureCommonDisease: + target.getClass().getCreatureStats(target).getSpells().purgeCommonDisease(); + break; + case ESM::MagicEffect::CureBlightDisease: + target.getClass().getCreatureStats(target).getSpells().purgeBlightDisease(); + break; + case ESM::MagicEffect::RemoveCurse: + target.getClass().getCreatureStats(target).getSpells().purgeCurses(); + break; + case ESM::MagicEffect::CureCorprusDisease: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Corprus); + break; + case ESM::MagicEffect::CurePoison: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Poison); + break; + case ESM::MagicEffect::CureParalyzation: + target.getClass().getCreatureStats(target).getActiveSpells().purgeEffect(target, ESM::MagicEffect::Paralyze); + break; + case ESM::MagicEffect::Dispel: + // Dispel removes entire spells at once + target.getClass().getCreatureStats(target).getActiveSpells().purge([magnitude=effect.mMagnitude] (const ActiveSpells::ActiveSpellParams& params) + { + if(params.getType() == ESM::ActiveSpells::Type_Temporary) + { + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(params.getId()); + if(spell && spell->mData.mType == ESM::Spell::ST_Spell) + return Misc::Rng::roll0to99() < magnitude; + } + return false; + }, target); + break; + case ESM::MagicEffect::AlmsiviIntervention: + case ESM::MagicEffect::DivineIntervention: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + auto marker = (effect.mEffectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker"; + world->teleportToClosestMarker(target, marker); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_end"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1); + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Mark: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + world->getPlayer().markPosition(target.getCell(), target.getRefData().getPosition()); + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::Recall: + if (target != getPlayer()) + invalid = true; + else if (world->isTeleportingEnabled()) + { + MWWorld::CellStore* markedCell = nullptr; + ESM::Position markedPosition; + + world->getPlayer().getMarkedPosition(markedCell, markedPosition); + if (markedCell) + { + MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName, markedPosition, false); + action.execute(target); + if(!caster.isEmpty()) + { + MWRender::Animation* anim = world->getAnimation(caster); + anim->removeEffect(effect.mEffectId); + } + } + } + else if (caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}"); + break; + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(caster.isEmpty() || !caster.getClass().isActor() || target == getPlayer() || (effect.mEffectId == ESM::MagicEffect::CommandCreature) == target.getClass().isNpc()) + invalid = true; + else if(effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel()) + { + MWMechanics::AiFollow package(caster, true); + target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(target.getClass().hasInventoryStore(target)) + { + auto& store = target.getClass().getInventoryStore(target); + store.unequipAll(target); + } + else + invalid = true; + break; + case ESM::MagicEffect::TurnUndead: + if(target.getClass().isNpc() || target.get()->mBase->mData.mType != ESM::Creature::Undead) + invalid = true; + else + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() + effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + if(!invalid && effect.mMagnitude > 0) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + creatureStats.getAiSequence().stopCombat(); + } + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + if(!target.isInCell()) + invalid = true; + else + effect.mArg = summonCreature(effect.mEffectId, target); + break; + case ESM::MagicEffect::BoundGloves: + if(!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + addBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + // left gauntlet added below + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + if(!target.getClass().hasInventoryStore(target)) + invalid = true; + else + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + addBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::FireDamage: + case ESM::MagicEffect::ShockDamage: + case ESM::MagicEffect::FrostDamage: + case ESM::MagicEffect::DamageHealth: + case ESM::MagicEffect::Poison: + case ESM::MagicEffect::DamageMagicka: + case ESM::MagicEffect::DamageFatigue: + if(!godmode) + { + int index = 0; + if(effect.mEffectId == ESM::MagicEffect::DamageMagicka) + index = 1; + else if(effect.mEffectId == ESM::MagicEffect::DamageFatigue) + index = 2; + // Damage "Dynamic" abilities reduce the base value + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, index, effect.mMagnitude); + else + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + adjustDynamicStat(target, index, -effect.mMagnitude, index == 2 && uncappedDamageFatigue); + if(index == 0) + receivedMagicDamage = true; + } + } + break; + case ESM::MagicEffect::DamageAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DamageSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + // Damage Skill abilities reduce base skill :todd: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + SkillValue& skill = npcStats.getSkill(effect.mArg); + // Damage Skill abilities reduce base skill :todd: + skill.setBase(std::max(skill.getBase() - effect.mMagnitude, 0.f)); + } + else + damageSkill(target, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::RestoreAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreSkill: + if(!target.getClass().isNpc()) + invalid = true; + else + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::RestoreHealth: + case ESM::MagicEffect::RestoreMagicka: + case ESM::MagicEffect::RestoreFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::RestoreHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::SunDamage: + { + // isInCell shouldn't be needed, but updateActor called during game start + if (!target.isInCell() || !target.getCell()->isExterior() || godmode) + break; + float time = world->getTimeStamp().getHour(); + float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); + float damageScale = 1.f - timeDiff / 7.f; + // When cloudy, the sun damage effect is halved + static float fMagicSunBlockedMult = world->getStore().get().find("fMagicSunBlockedMult")->mValue.getFloat(); + + int weather = world->getCurrentWeather(); + if (weather > 1) + damageScale *= fMagicSunBlockedMult; + float damage = effect.mMagnitude * damageScale; + adjustDynamicStat(target, 0, -damage); + if (damage > 0.f) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + if(!godmode) + { + static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); + int index = effect.mEffectId - ESM::MagicEffect::DrainHealth; + adjustDynamicStat(target, index, -effect.mMagnitude, uncappedDamageFatigue && index == 2); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude, false, true); + break; + case ESM::MagicEffect::DrainAttribute: + if(!godmode) + damageAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() + effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + damageSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + // Abilities affect base stats, but not for drain + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() + effect.mMagnitude); + } + else + fortifySkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbHealth: + case ESM::MagicEffect::AbsorbMagicka: + case ESM::MagicEffect::AbsorbFatigue: + if(!godmode) + { + int index = effect.mEffectId - ESM::MagicEffect::AbsorbHealth; + adjustDynamicStat(target, index, -effect.mMagnitude); + if(!caster.isEmpty()) + adjustDynamicStat(caster, index, effect.mMagnitude); + if(index == 0) + receivedMagicDamage = true; + } + break; + case ESM::MagicEffect::AbsorbAttribute: + if(!godmode) + { + damageAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + if(!target.getClass().isNpc()) + invalid = true; + else if(!godmode) + { + damageSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, effect.mMagnitude); + } + break; + case ESM::MagicEffect::DisintegrateArmor: + { + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (godmode) + break; + static const std::array priorities + { + MWWorld::InventoryStore::Slot_CarriedLeft, + MWWorld::InventoryStore::Slot_Cuirass, + MWWorld::InventoryStore::Slot_LeftPauldron, + MWWorld::InventoryStore::Slot_RightPauldron, + MWWorld::InventoryStore::Slot_LeftGauntlet, + MWWorld::InventoryStore::Slot_RightGauntlet, + MWWorld::InventoryStore::Slot_Helmet, + MWWorld::InventoryStore::Slot_Greaves, + MWWorld::InventoryStore::Slot_Boots + }; + for (const int priority : priorities) + { + if (disintegrateSlot(target, priority, effect.mMagnitude)) + break; + } + break; + } + case ESM::MagicEffect::DisintegrateWeapon: + if (!target.getClass().hasInventoryStore(target)) + { + invalid = true; + break; + } + if (!godmode) + disintegrateSlot(target, MWWorld::InventoryStore::Slot_CarriedRight, effect.mMagnitude); + break; + } +} + +bool 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; + 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); + } + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCorprusWorsens}"); + return false; + } + else if(effect.mEffectId == ESM::MagicEffect::Levitate && !world->isLevitationEnabled()) + { + if(target == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox ("#{sLevitateDisabled}"); + return true; + } + const auto* magicEffect = world->getStore().get().find(effect.mEffectId); + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + { + if(magicEffect->mData.mFlags & ESM::MagicEffect::Flags::AppliedOnce) + { + effect.mTimeLeft -= dt; + return false; + } + else if(!dt) + return false; + } + if(effect.mEffectId == ESM::MagicEffect::Lock) + { + if(target.getClass().canLock(target)) + { + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude + { + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}"); + target.getCellRef().lock(magnitude); + } + } + else + invalid = true; + } + else if(effect.mEffectId == ESM::MagicEffect::Open) + { + if(target.getClass().canLock(target)) + { + // Use the player instead of the caster for vanilla crime compatibility + MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target); + + MWRender::Animation* animation = world->getAnimation(target); + if(animation) + animation->addSpellCastGlow(magicEffect); + int magnitude = static_cast(roll(effect)); + if(target.getCellRef().getLockLevel() <= magnitude) + { + if(target.getCellRef().getLockLevel() > 0) + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); + + if(caster == getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); + } + target.getCellRef().unlock(); + } + else + { + MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f); + } + } + else + invalid = true; + } + else if(!target.getClass().isActor()) + { + invalid = true; + } + else + { + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + if(spellParams.getType() != ESM::ActiveSpells::Type_Ability && !(effect.mFlags & (ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Ignore_Resistances))) + { + 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; + } + float oldMagnitude = 0.f; + if(effect.mFlags & ESM::ActiveEffect::Flag_Applied) + oldMagnitude = effect.mMagnitude; + float magnitude = roll(effect); + //Note that there's an early out for Flag_Applied AppliedOnce effects so we don't have to exclude them here + effect.mMagnitude = magnitude; + if(!(magicEffect->mData.mFlags & (ESM::MagicEffect::Flags::NoMagnitude | ESM::MagicEffect::Flags::AppliedOnce))) + { + if(effect.mDuration != 0) + { + float mult = dt; + if(spellParams.getType() == ESM::ActiveSpells::Type_Consumable || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + mult = std::min(effect.mTimeLeft, dt); + effect.mMagnitude *= mult; + } + if(effect.mMagnitude == 0) + { + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + effect.mTimeLeft -= dt; + return false; + } + } + if(effect.mEffectId == ESM::MagicEffect::Corprus) + spellParams.worsen(); + else + applyMagicEffect(target, caster, spellParams, effect, invalid, receivedMagicDamage); + effect.mMagnitude = magnitude; + magnitudes.add(EffectKey(effect.mEffectId, effect.mArg), EffectParam(effect.mMagnitude - oldMagnitude)); + } + effect.mTimeLeft -= dt; + if(invalid) + { + effect.mTimeLeft = 0; + effect.mFlags |= ESM::ActiveEffect::Flag_Remove; + auto anim = world->getAnimation(target); + if(anim) + anim->removeEffect(effect.mEffectId); + } + else + effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; + if (receivedMagicDamage && target == getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + return false; +} + +void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + const auto world = MWBase::Environment::get().getWorld(); + auto& magnitudes = target.getClass().getCreatureStats(target).getMagicEffects(); + bool invalid; + switch(effect.mEffectId) + { + case ESM::MagicEffect::CommandCreature: + case ESM::MagicEffect::CommandHumanoid: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + { + auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); + auto it = std::find_if(seq.begin(), seq.end(), [&](const auto& package) + { + return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(package.get())->isCommanded(); + }); + if(it != seq.end()) + seq.erase(it); + } + break; + case ESM::MagicEffect::ExtraSpell: + if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) + target.getClass().getInventoryStore(target).autoEquip(target); + break; + case ESM::MagicEffect::TurnUndead: + { + auto& creatureStats = target.getClass().getCreatureStats(target); + Stat stat = creatureStats.getAiSetting(CreatureStats::AI_Flee); + stat.setModifier(static_cast(stat.getModifier() - effect.mMagnitude)); + creatureStats.setAiSetting(CreatureStats::AI_Flee, stat); + } + break; + case ESM::MagicEffect::FrenzyCreature: + case ESM::MagicEffect::FrenzyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::FrenzyCreature, CreatureStats::AI_Fight, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::CalmCreature: + case ESM::MagicEffect::CalmHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::CalmCreature, CreatureStats::AI_Fight, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::DemoralizeCreature: + case ESM::MagicEffect::DemoralizeHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::DemoralizeCreature, CreatureStats::AI_Flee, -effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::RallyCreature: + case ESM::MagicEffect::RallyHumanoid: + modifyAiSetting(target, effect, ESM::MagicEffect::RallyCreature, CreatureStats::AI_Flee, effect.mMagnitude, invalid); + break; + case ESM::MagicEffect::SummonScamp: + case ESM::MagicEffect::SummonClannfear: + case ESM::MagicEffect::SummonDaedroth: + case ESM::MagicEffect::SummonDremora: + case ESM::MagicEffect::SummonAncestralGhost: + case ESM::MagicEffect::SummonSkeletalMinion: + case ESM::MagicEffect::SummonBonewalker: + case ESM::MagicEffect::SummonGreaterBonewalker: + case ESM::MagicEffect::SummonBonelord: + case ESM::MagicEffect::SummonWingedTwilight: + case ESM::MagicEffect::SummonHunger: + case ESM::MagicEffect::SummonGoldenSaint: + case ESM::MagicEffect::SummonFlameAtronach: + case ESM::MagicEffect::SummonFrostAtronach: + case ESM::MagicEffect::SummonStormAtronach: + case ESM::MagicEffect::SummonCenturionSphere: + case ESM::MagicEffect::SummonFabricant: + case ESM::MagicEffect::SummonWolf: + case ESM::MagicEffect::SummonBear: + case ESM::MagicEffect::SummonBonewolf: + case ESM::MagicEffect::SummonCreature04: + case ESM::MagicEffect::SummonCreature05: + { + if(effect.mArg != -1) + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, effect.mArg); + auto& summons = target.getClass().getCreatureStats(target).getSummonedCreatureMap(); + auto [begin, end] = summons.equal_range(effect.mEffectId); + for(auto it = begin; it != end; ++it) + { + if(it->second == effect.mArg) + { + summons.erase(it); + break; + } + } + } + break; + case ESM::MagicEffect::BoundGloves: + removeBoundItem(world->getStore().get().find("sMagicBoundRightGauntletID")->mValue.getString(), target); + case ESM::MagicEffect::BoundDagger: + case ESM::MagicEffect::BoundLongsword: + case ESM::MagicEffect::BoundMace: + case ESM::MagicEffect::BoundBattleAxe: + case ESM::MagicEffect::BoundSpear: + case ESM::MagicEffect::BoundLongbow: + case ESM::MagicEffect::BoundCuirass: + case ESM::MagicEffect::BoundHelm: + case ESM::MagicEffect::BoundBoots: + case ESM::MagicEffect::BoundShield: + { + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + removeBoundItem(world->getStore().get().find(item)->mValue.getString(), target); + } + break; + case ESM::MagicEffect::DrainHealth: + case ESM::MagicEffect::DrainMagicka: + case ESM::MagicEffect::DrainFatigue: + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::DrainHealth, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyHealth: + case ESM::MagicEffect::FortifyMagicka: + case ESM::MagicEffect::FortifyFatigue: + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); + else + adjustDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude, true); + break; + case ESM::MagicEffect::DrainAttribute: + restoreAttribute(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyAttribute: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& creatureStats = target.getClass().getCreatureStats(target); + AttributeValue attr = creatureStats.getAttribute(effect.mArg); + attr.setBase(attr.getBase() - effect.mMagnitude); + creatureStats.setAttribute(effect.mArg, attr); + } + else + fortifyAttribute(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::DrainSkill: + restoreSkill(target, effect, effect.mMagnitude); + break; + case ESM::MagicEffect::FortifySkill: + // Abilities affect base stats, but not for drain + if(spellParams.getType() == ESM::ActiveSpells::Type_Ability) + { + auto& npcStats = target.getClass().getNpcStats(target); + auto& skill = npcStats.getSkill(effect.mArg); + skill.setBase(skill.getBase() - effect.mMagnitude); + } + else + fortifySkill(target, effect, -effect.mMagnitude); + break; + case ESM::MagicEffect::FortifyMaximumMagicka: + target.getClass().getCreatureStats(target).setNeedRecalcDynamicStats(true); + break; + case ESM::MagicEffect::AbsorbAttribute: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreAttribute(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifyAttribute(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::AbsorbSkill: + { + const auto caster = world->searchPtrViaActorId(spellParams.getCasterActorId()); + restoreSkill(target, effect, effect.mMagnitude); + if(!caster.isEmpty()) + fortifySkill(caster, effect, -effect.mMagnitude); + } + break; + case ESM::MagicEffect::Corprus: + { + int worsenings = spellParams.getWorsenings(); + spellParams.resetWorsenings(); + if(worsenings > 0) + { + for(const auto& otherEffect : spellParams.getEffects()) + { + if(isCorprusEffect(otherEffect, true)) + { + for(int i = 0; i < worsenings; i++) + removeMagicEffect(target, spellParams, otherEffect); + } + } + } + //Note that we remove the effects, but keep the params + target.getClass().getCreatureStats(target).getActiveSpells().purge([&spellParams] (const ActiveSpells::ActiveSpellParams& params, const auto&) + { + return &spellParams == ¶ms; + }, target); + } + break; + } +} + +void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spellParams, const ESM::ActiveEffect& effect) +{ + if(!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return; + 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)); + removeMagicEffect(target, spellParams, effect); + auto anim = world->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 new file mode 100644 index 0000000000..a9e7b066f3 --- /dev/null +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -0,0 +1,20 @@ +#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H +#define GAME_MWMECHANICS_SPELLEFFECTS_H + +#include "activespells.hpp" + +#include "../mwworld/ptr.hpp" + +// These functions should probably be split up into separate Lua functions for each magic effect when magic is dehardcoded. +// That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion. + +namespace MWMechanics +{ + // 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); + + // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce + void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect); +} + +#endif diff --git a/apps/openmw/mwmechanics/spelllist.hpp b/apps/openmw/mwmechanics/spelllist.hpp index 5920949d65..f5759fd7ee 100644 --- a/apps/openmw/mwmechanics/spelllist.hpp +++ b/apps/openmw/mwmechanics/spelllist.hpp @@ -16,12 +16,6 @@ namespace ESM namespace MWMechanics { - struct SpellParams - { - std::map mEffectRands; // - std::set mPurgedEffects; // indices of purged effects - }; - class Spells; /// Multiple instances of the same actor share the same spell list in Morrowind. diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index bd9c5f7cb3..974d297f10 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -31,21 +31,20 @@ namespace // if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc. if (effectFilter == -1) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->getId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - int effectId = effectIt->mEffectId; + int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get().find(effectId); - if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway + if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) @@ -64,15 +63,14 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->first != spellId) + if (it->getId() != spellId) continue; - const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second; - for (std::vector::const_iterator effectIt = params.mEffects.begin(); - effectIt != params.mEffects.end(); ++effectIt) + const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; + for (const auto& effect : params.getEffects()) { - if (effectIt->mDuration > duration) - duration = effectIt->mDuration; + if (effect.mDuration > duration) + duration = effect.mDuration; } } return duration; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index b871376003..6520ae3ab3 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -20,72 +20,33 @@ namespace MWMechanics { Spells::Spells() - : mSpellsChanged(false) { } Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells), - mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers), - mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects) + mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers) { if(mSpellList) mSpellList->addListener(this); } Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)), - mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)), - mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)), - mSourcedEffects(std::move(spells.mSourcedEffects)) + mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)) { if (mSpellList) mSpellList->updateListener(&spells, this); } - std::map::const_iterator Spells::begin() const + std::vector::const_iterator Spells::begin() const { return mSpells.begin(); } - std::map::const_iterator Spells::end() const + std::vector::const_iterator Spells::end() const { return mSpells.end(); } - void Spells::rebuildEffects() const - { - mEffects = MagicEffects(); - mSourcedEffects.clear(); - - for (const auto& iter : mSpells) - { - const ESM::Spell *spell = iter.first; - - if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight || - spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse) - { - int i=0; - for (const auto& effect : spell->mEffects.mList) - { - if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) - { - ++i; - continue; // effect was purged - } - - float random = 1.f; - if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) - random = iter.second.mEffectRands.at(i); - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random; - mEffects.add (effect, magnitude); - mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude); - - ++i; - } - } - } - } - bool Spells::hasSpell(const std::string &spell) const { return hasSpell(SpellList::getSpell(spell)); @@ -93,7 +54,7 @@ namespace MWMechanics bool Spells::hasSpell(const ESM::Spell *spell) const { - return mSpells.find(spell) != mSpells.end(); + return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) @@ -108,29 +69,8 @@ namespace MWMechanics void Spells::addSpell(const ESM::Spell* spell) { - if (mSpells.find (spell)==mSpells.end()) - { - std::map random; - - // Determine the random magnitudes (unless this is a castable spell, in which case - // they will be determined when the spell is cast) - if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) - { - for (unsigned int i=0; imEffects.mList.size();++i) - { - if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax) - { - int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin; - random[i] = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - } - } - } - - SpellParams params; - params.mEffectRands = random; - mSpells.emplace(spell, params); - mSpellsChanged = true; - } + if (!hasSpell(spell)) + mSpells.emplace_back(spell); } void Spells::remove (const std::string& spellId) @@ -145,27 +85,14 @@ namespace MWMechanics void Spells::removeSpell(const ESM::Spell* spell) { - const auto it = mSpells.find(spell); + const auto it = std::find(mSpells.begin(), mSpells.end(), spell); if(it != mSpells.end()) - { mSpells.erase(it); - mSpellsChanged = true; - } - } - - MagicEffects Spells::getMagicEffects() const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - return mEffects; } void Spells::removeAllSpells() { mSpells.clear(); - mSpellsChanged = true; } void Spells::clear(bool modifyBase) @@ -185,26 +112,10 @@ namespace MWMechanics return mSelectedSpell; } - bool Spells::isSpellActive(const std::string &id) const - { - if (id.empty()) - return false; - - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); - if (spell && hasSpell(spell)) - { - auto type = spell->mData.mType; - return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse); - } - - return false; - } - bool Spells::hasDisease(const ESM::Spell::SpellType type) const { - for (const auto& iter : mSpells) + for (const auto spell : mSpells) { - const ESM::Spell *spell = iter.first; if (spell->mData.mType == type) return true; } @@ -227,12 +138,11 @@ namespace MWMechanics std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { - const ESM::Spell *spell = iter->first; + const ESM::Spell *spell = *iter; if (filter(spell)) { - mSpells.erase(iter++); + iter = mSpells.erase(iter); purged.push_back(spell->mId); - mSpellsChanged = true; } else ++iter; @@ -261,43 +171,6 @@ namespace MWMechanics purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } - void Spells::removeEffects(const std::string &id) - { - if (isSpellActive(id)) - { - for (auto& spell : mSpells) - { - if (spell.first == SpellList::getSpell(id)) - { - for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++) - { - spell.second.mPurgedEffects.insert(i); - } - } - } - - mSpellsChanged = true; - } - } - - void Spells::visitEffectSources(EffectSourceVisitor &visitor) const - { - if (mSpellsChanged) { - rebuildEffects(); - mSpellsChanged = false; - } - - for (const auto& it : mSourcedEffects) - { - const ESM::Spell * spell = it.first; - for (const auto& effectIt : it.second) - { - // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here - visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); - } - } - } - bool Spells::hasCorprusEffect(const ESM::Spell *spell) { for (const auto& effectIt : spell->mEffects.mList) @@ -310,46 +183,6 @@ namespace MWMechanics return false; } - void Spells::purgeEffect(int effectId) - { - for (auto& spellIt : mSpells) - { - int i = 0; - for (auto& effectIt : spellIt.first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt.second.mPurgedEffects.insert(i); - mSpellsChanged = true; - } - ++i; - } - } - } - - void Spells::purgeEffect(int effectId, const std::string & sourceId) - { - // Effect source may be not a spell - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); - if (spell == nullptr) - return; - - auto spellIt = mSpells.find(spell); - if (spellIt == mSpells.end()) - return; - - int index = 0; - for (auto& effectIt : spellIt->first->mEffects.mList) - { - if (effectIt.mEffectID == effectId) - { - spellIt->second.mPurgedEffects.insert(index); - mSpellsChanged = true; - } - ++index; - } - } - bool Spells::canUsePower(const ESM::Spell* spell) const { const auto it = mUsedPowers.find(spell); @@ -365,17 +198,16 @@ namespace MWMechanics { const auto& baseSpells = mSpellList->getSpells(); - for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) + for (const std::string& id : state.mSpells) { // Discard spells that are no longer available due to changed content files - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if (spell) { - mSpells[spell].mEffectRands = it->second.mEffectRands; - mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; + addSpell(spell); - if (it->first == state.mSelectedSpell) - mSelectedSpell = it->first; + if (id == state.mSelectedSpell) + mSelectedSpell = id; } } // Add spells from the base record @@ -394,31 +226,6 @@ namespace MWMechanics mUsedPowers[spell] = MWWorld::TimeStamp(it->second); } - for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) - { - const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); - if (!spell) - continue; - - CorprusStats stats; - - int worsening = state.mCorprusSpells.at(it->first).mWorsenings; - - for (int i=0; imEffects.mList) - { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = worsening; - } - stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening); - - creatureStats->addCorprusSpell(it->first, stats); - } - - mSpellsChanged = true; - // Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach. for (std::map >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) @@ -457,16 +264,13 @@ namespace MWMechanics void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); - for (const auto& it : mSpells) + for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record - if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) || - std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end()) + if((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) || + std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end()) { - ESM::SpellState::SpellParams params; - params.mEffectRands = it.second.mEffectRands; - params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.emplace(it.first->mId, params); + state.mSpells.emplace_back(spell->mId); } } diff --git a/apps/openmw/mwmechanics/spells.hpp b/apps/openmw/mwmechanics/spells.hpp index 9737b72cd0..29f505d369 100644 --- a/apps/openmw/mwmechanics/spells.hpp +++ b/apps/openmw/mwmechanics/spells.hpp @@ -29,18 +29,13 @@ namespace MWMechanics class Spells { std::shared_ptr mSpellList; - std::map mSpells; + std::vector mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) std::string mSelectedSpell; std::map mUsedPowers; - mutable bool mSpellsChanged; - mutable MagicEffects mEffects; - mutable std::map mSourcedEffects; - void rebuildEffects() const; - bool hasDisease(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); @@ -52,8 +47,6 @@ namespace MWMechanics friend class SpellList; public: - using TIterator = std::map::const_iterator; - Spells(); Spells(const Spells&); @@ -64,9 +57,6 @@ namespace MWMechanics static bool hasCorprusEffect(const ESM::Spell *spell); - void purgeEffect(int effectId); - void purgeEffect(int effectId, const std::string & sourceId); - bool canUsePower (const ESM::Spell* spell) const; void usePower (const ESM::Spell* spell); @@ -75,9 +65,9 @@ namespace MWMechanics void purgeCorprusDisease(); void purgeCurses(); - TIterator begin() const; + std::vector::const_iterator begin() const; - TIterator end() const; + std::vector::const_iterator end() const; bool hasSpell(const std::string& spell) const; bool hasSpell(const ESM::Spell* spell) const; @@ -92,9 +82,6 @@ namespace MWMechanics ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty string). - MagicEffects getMagicEffects() const; - ///< Return sum of magic effects resulting from abilities, blights, deseases and curses. - void clear(bool modifyBase = false); ///< Remove all spells of al types. @@ -104,17 +91,10 @@ namespace MWMechanics const std::string getSelectedSpell() const; ///< May return an empty string. - bool isSpellActive(const std::string& id) const; - ///< Are we under the effects of the given spell ID? - bool hasCommonDisease() const; bool hasBlightDisease() const; - void removeEffects(const std::string& id); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const; - void readState (const ESM::SpellState& state, CreatureStats* creatureStats); void writeState (ESM::SpellState& state) const; diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index d90545fc60..c1923d4337 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -60,90 +60,60 @@ namespace MWMechanics return std::string(); } - UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor) - : mActor(actor) + int summonCreature(int effectId, const MWWorld::Ptr& summoner) { - } - - void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) - { - if (isSummoningEffect(key.mId) && magnitude > 0) + std::string creatureID = getSummonedCreature(effectId); + int creatureActorId = -1; + if (!creatureID.empty()) { - mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); - } - } - - void UpdateSummonedCreatures::process(bool cleanup) - { - MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - - for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) - { - bool found = creatureMap.find(*it) != creatureMap.end(); - if (!found) + try { - std::string creatureID = getSummonedCreature(it->mEffectId); - if (!creatureID.empty()) + auto world = MWBase::Environment::get().getWorld(); + MWWorld::ManualRef ref(world->getStore(), creatureID, 1); + + MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); + + // Make the summoned creature follow its master and help in fights + AiFollow package(summoner); + summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); + creatureActorId = summonedCreatureStats.getActorId(); + + MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f); + + MWRender::Animation* anim = world->getAnimation(placed); + if (anim) { - int creatureActorId = -1; - try - { - MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1); - - MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); - - // Make the summoned creature follow its master and help in fights - AiFollow package(mActor); - summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); - creatureActorId = summonedCreatureStats.getActorId(); - - MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f); - - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed); - if (anim) - { - const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get() - .search("VFX_Summon_Start"); - if (fx) - anim->addEffect("meshes\\" + fx->mModel, -1, false); - } - } - catch (std::exception& e) - { - Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); - // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log - } - - creatureMap.emplace(*it, creatureActorId); + const ESM::Static* fx = world->getStore().get().search("VFX_Summon_Start"); + if (fx) + anim->addEffect("meshes\\" + fx->mModel, -1, false); } } - } - - // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) - { - bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); - if (!found) + catch (std::exception& e) { - // Effect has ended - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); - creatureMap.erase(it++); - continue; + Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what(); + // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } - ++it; + + summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId); } + return creatureActorId; + } + + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup) + { + MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner); + auto& creatureMap = creatureStats.getSummonedCreatureMap(); std::vector graveyard = creatureStats.getSummonedCreatureGraveyard(); creatureStats.getSummonedCreatureGraveyard().clear(); for (const int creature : graveyard) - MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature); + MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature); if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (auto it = creatureMap.begin(); it != creatureMap.end(); ) { if(it->second == -1) { @@ -155,21 +125,22 @@ namespace MWMechanics if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished()) { // Purge the magic effect so a new creature can be summoned if desired - purgeSummonEffect(mActor, *it); + auto summon = *it; creatureMap.erase(it++); + purgeSummonEffect(summoner, summon); } else ++it; } } - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); - creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex); - creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId); - if (summoner.getClass().hasInventoryStore(summoner)) - summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex); + creatureStats.getActiveSpells().purge([summon] (const auto& spell, const auto& effect) + { + return effect.mEffectId == summon.first && effect.mArg == summon.second; + }, summoner); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second); } diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index 3c3e18a96b..3186eef986 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -11,32 +11,15 @@ namespace MWMechanics { - class CreatureStats; - bool isSummoningEffect(int effectId); std::string getSummonedCreature(int effectId); - void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); + void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); - struct UpdateSummonedCreatures : public EffectSourceVisitor - { - UpdateSummonedCreatures(const MWWorld::Ptr& actor); - virtual ~UpdateSummonedCreatures() = default; - - void visit (MWMechanics::EffectKey key, int effectIndex, - const std::string& sourceName, const std::string& sourceId, int casterActorId, - float magnitude, float remainingTime = -1, float totalTime = -1) override; - - /// To call after all effect sources have been visited - void process(bool cleanup); - - private: - MWWorld::Ptr mActor; - - std::set mActiveEffects; - }; + int summonCreature(int effectId, const MWWorld::Ptr& summoner); + void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif diff --git a/apps/openmw/mwmechanics/tickableeffects.cpp b/apps/openmw/mwmechanics/tickableeffects.cpp deleted file mode 100644 index 5056179f8f..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.cpp +++ /dev/null @@ -1,216 +0,0 @@ -#include "tickableeffects.hpp" - -#include - -#include "../mwbase/environment.hpp" -#include "../mwbase/windowmanager.hpp" -#include "../mwbase/world.hpp" - -#include "../mwworld/cellstore.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/containerstore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/inventorystore.hpp" - -#include "actorutil.hpp" -#include "npcstats.hpp" - -namespace MWMechanics -{ - void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false) - { - DynamicStat stat = creatureStats.getDynamic(index); - stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero); - creatureStats.setDynamic(index, stat); - } - - bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate) - { - if (!ptr.getClass().hasInventoryStore(ptr)) - return false; - - MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); - MWWorld::ContainerStoreIterator item = inv.getSlot(slot); - - if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon)) - { - if (!item->getClass().hasItemHealth(*item)) - return false; - int charge = item->getClass().getItemHealth(*item); - if (charge == 0) - return false; - - // Store remainder of disintegrate amount (automatically subtracted if > 1) - item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate)); - - charge = item->getClass().getItemHealth(*item); - charge -= std::min(static_cast(disintegrate), charge); - item->getCellRef().setCharge(charge); - - if (charge == 0) - { - // Will unequip the broken item and try to find a replacement - if (ptr != getPlayer()) - inv.autoEquip(ptr); - else - inv.unequipItem(*item, ptr); - } - - return true; - } - - return false; - } - - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude) - { - if (magnitude == 0.f) - return false; - - bool receivedMagicDamage = false; - bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - switch (effectKey.mId) - { - case ESM::MagicEffect::DamageAttribute: - { - if (godmode) - break; - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.damage(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreAttribute: - { - AttributeValue attr = creatureStats.getAttribute(effectKey.mArg); - attr.restore(magnitude); - creatureStats.setAttribute(effectKey.mArg, attr); - break; - } - case ESM::MagicEffect::RestoreHealth: - case ESM::MagicEffect::RestoreMagicka: - case ESM::MagicEffect::RestoreFatigue: - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude); - break; - case ESM::MagicEffect::DamageHealth: - if (godmode) - break; - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude); - break; - - case ESM::MagicEffect::DamageMagicka: - case ESM::MagicEffect::DamageFatigue: - { - if (godmode) - break; - int index = effectKey.mId-ESM::MagicEffect::DamageHealth; - static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game"); - adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue); - break; - } - case ESM::MagicEffect::AbsorbHealth: - if (!godmode || magnitude <= 0) - { - if (magnitude > 0.f) - receivedMagicDamage = true; - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - } - break; - - case ESM::MagicEffect::AbsorbMagicka: - case ESM::MagicEffect::AbsorbFatigue: - if (!godmode || magnitude <= 0) - adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude); - break; - - case ESM::MagicEffect::DisintegrateArmor: - { - if (godmode) - break; - static const std::array priorities - { - MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, - MWWorld::InventoryStore::Slot_RightGauntlet, - MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots - }; - for (const int priority : priorities) - { - if (disintegrateSlot(actor, priority, magnitude)) - break; - } - - break; - } - case ESM::MagicEffect::DisintegrateWeapon: - if (!godmode) - disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude); - break; - - case ESM::MagicEffect::SunDamage: - { - // isInCell shouldn't be needed, but updateActor called during game start - if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode) - break; - float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour(); - float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13))); - float damageScale = 1.f - timeDiff / 7.f; - // When cloudy, the sun damage effect is halved - static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get().find( - "fMagicSunBlockedMult")->mValue.getFloat(); - - int weather = MWBase::Environment::get().getWorld()->getCurrentWeather(); - if (weather > 1) - damageScale *= fMagicSunBlockedMult; - - adjustDynamicStat(creatureStats, 0, -magnitude * damageScale); - if (magnitude * damageScale > 0.f) - receivedMagicDamage = true; - - break; - } - - case ESM::MagicEffect::FireDamage: - case ESM::MagicEffect::ShockDamage: - case ESM::MagicEffect::FrostDamage: - case ESM::MagicEffect::Poison: - { - if (godmode) - break; - adjustDynamicStat(creatureStats, 0, -magnitude); - receivedMagicDamage = true; - break; - } - - case ESM::MagicEffect::DamageSkill: - case ESM::MagicEffect::RestoreSkill: - { - if (!actor.getClass().isNpc()) - break; - if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill) - break; - NpcStats &npcStats = actor.getClass().getNpcStats(actor); - SkillValue& skill = npcStats.getSkill(effectKey.mArg); - if (effectKey.mId == ESM::MagicEffect::RestoreSkill) - skill.restore(magnitude); - else - skill.damage(magnitude); - break; - } - - default: - return false; - } - - if (receivedMagicDamage && actor == getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - return true; - } -} diff --git a/apps/openmw/mwmechanics/tickableeffects.hpp b/apps/openmw/mwmechanics/tickableeffects.hpp deleted file mode 100644 index ccd42ca19b..0000000000 --- a/apps/openmw/mwmechanics/tickableeffects.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef MWMECHANICS_TICKABLEEFFECTS_H -#define MWMECHANICS_TICKABLEEFFECTS_H - -namespace MWWorld -{ - class Ptr; -} - -namespace MWMechanics -{ - class CreatureStats; - struct EffectKey; - - /// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed - /// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation. - /// @return Was the effect a tickable effect with a magnitude? - bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude); -} - -#endif diff --git a/apps/openmw/mwphysics/actor.cpp b/apps/openmw/mwphysics/actor.cpp index 21736543ab..e140141e32 100644 --- a/apps/openmw/mwphysics/actor.cpp +++ b/apps/openmw/mwphysics/actor.cpp @@ -19,9 +19,9 @@ namespace MWPhysics { -Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler) - : mStandingOnPtr(nullptr), mCanWaterWalk(false), mWalkingOnWater(false) - , mCollisionObject(nullptr), mMeshTranslation(shape->mCollisionBox.center), mHalfExtents(shape->mCollisionBox.extents) +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} , mForce(0.f, 0.f, 0.f), mOnGround(true), mOnSlope(false) , mInternalCollisionMode(true) @@ -33,7 +33,7 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic // We can not create actor without collisions - he will fall through the ground. // In this case we should autogenerate collision box based on mesh shape // (NPCs have bodyparts and use a different approach) - if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f) + if (!ptr.getClass().isNpc() && mOriginalHalfExtents.length2() == 0.f) { if (shape->mCollisionShape) { @@ -43,19 +43,19 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic btVector3 max; shape->mCollisionShape->getAabb(transform, min, max); - mHalfExtents.x() = (max[0] - min[0])/2.f; - mHalfExtents.y() = (max[1] - min[1])/2.f; - mHalfExtents.z() = (max[2] - min[2])/2.f; + mOriginalHalfExtents.x() = (max[0] - min[0])/2.f; + mOriginalHalfExtents.y() = (max[1] - min[1])/2.f; + mOriginalHalfExtents.z() = (max[2] - min[2])/2.f; - mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z()); + mMeshTranslation = osg::Vec3f(0.f, 0.f, mOriginalHalfExtents.z()); } - if (mHalfExtents.length2() == 0.f) + if (mOriginalHalfExtents.length2() == 0.f) Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\"."; } - mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents))); - mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2; + mShape.reset(new btBoxShape(Misc::Convert::toBullet(mOriginalHalfExtents))); + mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mOriginalHalfExtents.x() - mOriginalHalfExtents.y()) < 2.2; mConvexShape = static_cast(mShape.get()); @@ -82,7 +82,7 @@ Actor::~Actor() void Actor::enableCollisionMode(bool collision) { - mInternalCollisionMode.store(collision, std::memory_order_release); + mInternalCollisionMode = collision; } void Actor::enableCollisionBody(bool collision) @@ -124,11 +124,13 @@ void Actor::updatePosition() mPositionOffset = osg::Vec3f(); mStandingOnPtr = nullptr; mSkipCollisions = true; + mSkipSimulation = true; } void Actor::setSimulationPosition(const osg::Vec3f& position) { - mSimulationPosition = position; + if (!std::exchange(mSkipSimulation, false)) + mSimulationPosition = position; } osg::Vec3f Actor::getSimulationPosition() const @@ -145,18 +147,20 @@ void Actor::updateCollisionObjectPosition() { std::scoped_lock lock(mPositionMutex); mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); - osg::Vec3f scaledTranslation = mRotation * osg::componentMultiply(mMeshTranslation, mScale); - osg::Vec3f newPosition = scaledTranslation + mPosition; - mLocalTransform.setOrigin(Misc::Convert::toBullet(newPosition)); - mLocalTransform.setRotation(Misc::Convert::toBullet(mRotation)); - mCollisionObject->setWorldTransform(mLocalTransform); + osg::Vec3f newPosition = getScaledMeshTranslation() + mPosition; + + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(newPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); + mWorldPositionChanged = false; } osg::Vec3f Actor::getCollisionObjectPosition() const { std::scoped_lock lock(mPositionMutex); - return Misc::Convert::toOsg(mLocalTransform.getOrigin()); + return getScaledMeshTranslation() + mPosition; } bool Actor::setPosition(const osg::Vec3f& position) @@ -216,27 +220,26 @@ void Actor::updateScale() mPtr.getClass().adjustScale(mPtr, scaleVec, false); mScale = scaleVec; + mHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); scaleVec = osg::Vec3f(scale,scale,scale); mPtr.getClass().adjustScale(mPtr, scaleVec, true); - mRenderingScale = scaleVec; + mRenderingHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); } osg::Vec3f Actor::getHalfExtents() const { - std::scoped_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mScale); + return mHalfExtents; } osg::Vec3f Actor::getOriginalHalfExtents() const { - return mHalfExtents; + return mOriginalHalfExtents; } osg::Vec3f Actor::getRenderingHalfExtents() const { - std::scoped_lock lock(mPositionMutex); - return osg::componentMultiply(mHalfExtents, mRenderingScale); + return mRenderingHalfExtents; } void Actor::setInertialForce(const osg::Vec3f &force) @@ -246,22 +249,22 @@ void Actor::setInertialForce(const osg::Vec3f &force) void Actor::setOnGround(bool grounded) { - mOnGround.store(grounded, std::memory_order_release); + mOnGround = grounded; } void Actor::setOnSlope(bool slope) { - mOnSlope.store(slope, std::memory_order_release); + mOnSlope = slope; } bool Actor::isWalkingOnWater() const { - return mWalkingOnWater.load(std::memory_order_acquire); + return mWalkingOnWater; } void Actor::setWalkingOnWater(bool walkingOnWater) { - mWalkingOnWater.store(walkingOnWater, std::memory_order_release); + mWalkingOnWater = walkingOnWater; } void Actor::setCanWaterWalk(bool waterWalk) diff --git a/apps/openmw/mwphysics/actor.hpp b/apps/openmw/mwphysics/actor.hpp index dd4ea45714..0846401c1d 100644 --- a/apps/openmw/mwphysics/actor.hpp +++ b/apps/openmw/mwphysics/actor.hpp @@ -1,7 +1,6 @@ #ifndef OPENMW_MWPHYSICS_ACTOR_H #define OPENMW_MWPHYSICS_ACTOR_H -#include #include #include @@ -27,7 +26,7 @@ namespace MWPhysics class Actor final : public PtrHolder { public: - Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler); + Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, PhysicsTaskScheduler* scheduler, bool canWaterWalk); ~Actor() override; /** @@ -37,7 +36,7 @@ namespace MWPhysics bool getCollisionMode() const { - return mInternalCollisionMode.load(std::memory_order_acquire); + return mInternalCollisionMode; } btConvexShape* getConvexShape() const { return mConvexShape; } @@ -74,9 +73,6 @@ namespace MWPhysics */ osg::Vec3f getOriginalHalfExtents() const; - /// Returns the mesh translation, scaled and rotated as necessary - osg::Vec3f getScaledMeshTranslation() const; - /** * Returns the position of the collision body * @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space. @@ -126,19 +122,14 @@ namespace MWPhysics bool getOnGround() const { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnGround.load(std::memory_order_acquire); + return mInternalCollisionMode && mOnGround; } void setOnSlope(bool slope); bool getOnSlope() const { - return mInternalCollisionMode.load(std::memory_order_acquire) && mOnSlope.load(std::memory_order_acquire); - } - - btCollisionObject* getCollisionObject() const - { - return mCollisionObject.get(); + return mInternalCollisionMode && mOnSlope; } /// Sets whether this actor should be able to collide with the water surface @@ -181,22 +172,24 @@ namespace MWPhysics void addCollisionMask(int collisionMask); int getCollisionMask() const; + /// Returns the mesh translation, scaled and rotated as necessary + osg::Vec3f getScaledMeshTranslation() const; + bool mCanWaterWalk; - std::atomic mWalkingOnWater; + bool mWalkingOnWater; bool mRotationallyInvariant; std::unique_ptr mShape; btConvexShape* mConvexShape; - std::unique_ptr mCollisionObject; - osg::Vec3f mMeshTranslation; + osg::Vec3f mOriginalHalfExtents; osg::Vec3f mHalfExtents; + osg::Vec3f mRenderingHalfExtents; osg::Quat mRotation; osg::Vec3f mScale; - osg::Vec3f mRenderingScale; osg::Vec3f mSimulationPosition; osg::Vec3f mPosition; osg::Vec3f mPreviousPosition; @@ -204,16 +197,16 @@ namespace MWPhysics osg::Vec3f mVelocity; bool mWorldPositionChanged; bool mSkipCollisions; - btTransform mLocalTransform; + bool mSkipSimulation; mutable std::mutex mPositionMutex; unsigned int mStuckFrames; osg::Vec3f mLastStuckPosition; osg::Vec3f mForce; - std::atomic mOnGround; - std::atomic mOnSlope; - std::atomic mInternalCollisionMode; + bool mOnGround; + bool mOnSlope; + bool mInternalCollisionMode; bool mExternalCollisionMode; PhysicsTaskScheduler* mTaskScheduler; diff --git a/apps/openmw/mwphysics/actorconvexcallback.cpp b/apps/openmw/mwphysics/actorconvexcallback.cpp index ef52e07c21..672af05058 100644 --- a/apps/openmw/mwphysics/actorconvexcallback.cpp +++ b/apps/openmw/mwphysics/actorconvexcallback.cpp @@ -77,10 +77,8 @@ namespace MWPhysics auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); if (!projectileHolder->isActive()) return btScalar(1); - auto* targetHolder = static_cast(mMe->getUserPointer()); - const MWWorld::Ptr target = targetHolder->getPtr(); - if (projectileHolder->isValidTarget(target)) - projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); + if (projectileHolder->isValidTarget(mMe)) + projectileHolder->hit(mMe, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); return btScalar(1); } diff --git a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp index 32d97d6c75..3f6cb2b727 100644 --- a/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp +++ b/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp @@ -7,6 +7,7 @@ #include "../mwworld/class.hpp" +#include "collisiontype.hpp" #include "ptrholder.hpp" namespace MWPhysics @@ -19,17 +20,14 @@ namespace MWPhysics btScalar ClosestNotMeRayResultCallback::addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { - if (rayResult.m_collisionObject == mMe) + const auto* hitObject = rayResult.m_collisionObject; + if (hitObject == mMe) return 1.f; - if (!mTargets.empty()) + if (hitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { - if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end())) - { - auto* holder = static_cast(rayResult.m_collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 1.f; - } + if ((std::find(mTargets.begin(), mTargets.end(), hitObject) == mTargets.end())) + return 1.f; } return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); diff --git a/apps/openmw/mwphysics/collisiontype.hpp b/apps/openmw/mwphysics/collisiontype.hpp index 0d6a32fc09..e69534cf68 100644 --- a/apps/openmw/mwphysics/collisiontype.hpp +++ b/apps/openmw/mwphysics/collisiontype.hpp @@ -10,7 +10,8 @@ enum CollisionType { CollisionType_Actor = 1<<2, CollisionType_HeightMap = 1<<3, CollisionType_Projectile = 1<<4, - CollisionType_Water = 1<<5 + CollisionType_Water = 1<<5, + CollisionType_Default = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door }; } diff --git a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp index 7744af14b5..3531cc8eb8 100644 --- a/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp +++ b/apps/openmw/mwphysics/deepestnotmecontacttestresultcallback.cpp @@ -6,6 +6,7 @@ #include "../mwworld/class.hpp" +#include "collisiontype.hpp" #include "ptrholder.hpp" namespace MWPhysics @@ -23,14 +24,10 @@ namespace MWPhysics const btCollisionObject* collisionObject = col1Wrap->m_collisionObject; if (collisionObject != mMe) { - if (!mTargets.empty()) + if (collisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), collisionObject) == mTargets.end())) - { - PtrHolder* holder = static_cast(collisionObject->getUserPointer()); - if (holder && !holder->getPtr().isEmpty() && holder->getPtr().getClass().isActor()) - return 0.f; - } + return 0.f; } btScalar distsqr = mOrigin.distance2(cp.getPositionWorldOnA()); diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index 275325cf67..fc8725f5f4 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -22,28 +22,36 @@ namespace MWPhysics return nearest.distance(position) < radius; } + template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: HasSphereCollisionCallback(const btVector3& position, const btScalar radius, btCollisionObject* object, - const int mask, const int group) + const int mask, const int group, OnCollision* onCollision) : mPosition(position), mRadius(radius), mCollisionObject(object), mCollisionFilterMask(mask), - mCollisionFilterGroup(group) + mCollisionFilterGroup(group), + mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult) + if (mResult && mOnCollision == nullptr) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (collisionObject == mCollisionObject) + if (collisionObject == mCollisionObject + || !needsCollision(*proxy) + || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; - if (needsCollision(*proxy)) - mResult = testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius); + mResult = true; + if (mOnCollision != nullptr) + { + (*mOnCollision)(collisionObject); + return true; + } return !mResult; } @@ -58,6 +66,7 @@ namespace MWPhysics btCollisionObject* mCollisionObject; int mCollisionFilterMask; int mCollisionFilterGroup; + OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index fd0e090fcb..df9239dd83 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -118,69 +118,52 @@ namespace MWPhysics void MovementSolver::move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData) { - auto* physicActor = actor.mActorRaw; - const ESM::Position& refpos = actor.mRefpos; - // Early-out for totally static creatures - // (Not sure if gravity should still apply?) - { - const auto ptr = physicActor->getPtr(); - if (!ptr.getClass().isMobile(ptr)) - return; - } - // Reset per-frame data - physicActor->setWalkingOnWater(false); + actor.mWalkingOnWater = false; // Anything to collide with? - if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) + if(actor.mSkipCollisionDetection) { - actor.mPosition += (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * - osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1)) + actor.mPosition += (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * + osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1)) ) * actor.mMovement * time; return; } - const btCollisionObject *colobj = physicActor->getCollisionObject(); - // Adjust for collision mesh offset relative to actor's "location" // (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own) // for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation // if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() - osg::Vec3f halfExtents = physicActor->getHalfExtents(); - actor.mPosition.z() += halfExtents.z(); // vanilla-accurate + actor.mPosition.z() += actor.mHalfExtentsZ; // vanilla-accurate - static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); - float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); + float swimlevel = actor.mSwimLevel + actor.mHalfExtentsZ; ActorTracer tracer; - osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; - if (actor.mPosition.z() < swimlevel || actor.mFlying) - { - velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; - } - else - { - velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; - - if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) - || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) - inertia = velocity; - else if (!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity = velocity + inertia; - } - // Dead and paralyzed actors underwater will float to the surface, // if the CharacterController tells us to do so - if (actor.mMovement.z() > 0 && actor.mFloatToSurface && actor.mPosition.z() < swimlevel) + if (actor.mMovement.z() > 0 && actor.mInert && actor.mPosition.z() < swimlevel) + { velocity = osg::Vec3f(0,0,1) * 25; + } + else if (actor.mPosition.z() < swimlevel || actor.mFlying) + { + velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; + } + else + { + velocity = (osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; - if (actor.mWantJump) - actor.mDidJump = true; + if ((velocity.z() > 0.f && actor.mIsOnGround && !actor.mIsOnSlope) + || (velocity.z() > 0.f && velocity.z() + actor.mInertia.z() <= -velocity.z() && actor.mIsOnSlope)) + actor.mInertia = velocity; + else if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity = velocity + actor.mInertia; + } // Now that we have the effective movement vector, apply wind forces to it - if (worldData.mIsInStorm) + if (worldData.mIsInStorm && velocity.length() > 0) { osg::Vec3f stormDirection = worldData.mStormDirection; float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); @@ -188,7 +171,7 @@ namespace MWPhysics velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f)); } - Stepper stepper(collisionWorld, colobj); + Stepper stepper(collisionWorld, actor.mCollisionObject); osg::Vec3f origVelocity = velocity; osg::Vec3f newPosition = actor.mPosition; /* @@ -197,7 +180,6 @@ namespace MWPhysics * The initial velocity was set earlier (see above). */ float remainingTime = time; - bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; int numTimesSlid = 0; osg::Vec3f lastSlideNormal(0,0,1); @@ -207,9 +189,10 @@ namespace MWPhysics for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.0001f; ++iterations) { osg::Vec3f nextpos = newPosition + velocity * remainingTime; + bool underwater = newPosition.z() < swimlevel; // If not able to fly, don't allow to swim up into the air - if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel) + if(!actor.mFlying && nextpos.z() > swimlevel && underwater) { const osg::Vec3f down(0,0,-1); velocity = reject(velocity, down); @@ -220,7 +203,7 @@ namespace MWPhysics if((newPosition - nextpos).length2() > 0.0001) { // trace to where character would go if there were no obstructions - tracer.doTrace(colobj, newPosition, nextpos, collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld); // check for obstructions if(tracer.mFraction >= 1.0f) @@ -241,11 +224,10 @@ namespace MWPhysics break; } - if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) - seenGround = true; + bool seenGround = !actor.mFlying && !underwater && ((actor.mIsOnGround && !actor.mIsOnSlope) || isWalkableSlope(tracer.mPlaneNormal)); // We hit something. Check if we can step up. - float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); + float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) @@ -256,9 +238,7 @@ namespace MWPhysics } if (usedStepLogic) { - // don't let pure water creatures move out of water after stepMove - const auto ptr = physicActor->getPtr(); - if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel) + if (actor.mIsAquatic && newPosition.z() + actor.mHalfExtentsZ > actor.mWaterlevel) newPosition = oldPosition; else if(!actor.mFlying && actor.mPosition.z() >= swimlevel) forceGroundTest = true; @@ -317,7 +297,7 @@ namespace MWPhysics // version of surface rejection for acute crevices/seams auto averageNormal = bestNormal + planeNormal; averageNormal.normalize(); - tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld); newPosition = (newPosition + tracer.mEndPos)/2.0; usedSeamLogic = true; @@ -333,7 +313,7 @@ namespace MWPhysics // but this is along the collision normal if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f)) { - tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld); newPosition = (newPosition + tracer.mEndPos)/2.0; } @@ -353,26 +333,22 @@ namespace MWPhysics bool isOnGround = false; bool isOnSlope = false; - if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel)) + if (forceGroundTest || (actor.mInertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; - auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0); + auto dropDistance = 2*sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance); - tracer.doTrace(colobj, from, to, collisionWorld); + tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld); if(tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) { isOnGround = true; isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); + actor.mStandingOn = tracer.mHitObject; - const btCollisionObject* standingOn = tracer.mHitObject; - PtrHolder* ptrHolder = static_cast(standingOn->getUserPointer()); - if (ptrHolder) - actor.mStandingOn = ptrHolder->getPtr(); - - if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) - physicActor->setWalkingOnWater(true); + if (actor.mStandingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) + actor.mWalkingOnWater = true; if (!actor.mFlying && !isOnSlope) { if (tracer.mFraction*dropDistance > sGroundOffset) @@ -380,7 +356,7 @@ namespace MWPhysics else { newPosition.z() = tracer.mEndPos.z(); - tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); + tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld); newPosition = (newPosition+tracer.mEndPos)/2.0; } } @@ -395,7 +371,7 @@ namespace MWPhysics } } // forcibly treat stuck actors as if they're on flat ground because buggy collisions when inside of things can/will break ground detection - if(physicActor->getStuckFrames() > 0) + if(actor.mStuckFrames > 0) { isOnGround = true; isOnSlope = false; @@ -403,24 +379,23 @@ namespace MWPhysics } if((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) - physicActor->setInertialForce(osg::Vec3f(0.f, 0.f, 0.f)); + actor.mInertia = osg::Vec3f(0.f, 0.f, 0.f); else { - inertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; - if (inertia.z() < 0) - inertia.z() *= actor.mSlowFall; + actor.mInertia.z() -= time * Constants::GravityConst * Constants::UnitsPerMeter; + if (actor.mInertia.z() < 0) + actor.mInertia.z() *= actor.mSlowFall; if (actor.mSlowFall < 1.f) { - inertia.x() *= actor.mSlowFall; - inertia.y() *= actor.mSlowFall; + actor.mInertia.x() *= actor.mSlowFall; + actor.mInertia.y() *= actor.mSlowFall; } - physicActor->setInertialForce(inertia); } - physicActor->setOnGround(isOnGround); - physicActor->setOnSlope(isOnSlope); + actor.mIsOnGround = isOnGround; + actor.mIsOnSlope = isOnSlope; actor.mPosition = newPosition; // remove what was added earlier in compensating for doTrace not taking interior transformation into account - actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate + actor.mPosition.z() -= actor.mHalfExtentsZ; // vanilla-accurate } btVector3 addMarginToDelta(btVector3 delta) @@ -432,53 +407,47 @@ namespace MWPhysics void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) { - const auto& ptr = actor.mActorRaw->getPtr(); - if (!ptr.getClass().isMobile(ptr)) + if(actor.mSkipCollisionDetection) // noclipping/tcl return; - auto* physicActor = actor.mActorRaw; - if(!physicActor->getCollisionMode() || actor.mSkipCollisionDetection) // noclipping/tcl - return; - - auto* collisionObject = physicActor->getCollisionObject(); auto tempPosition = actor.mPosition; - if(physicActor->getStuckFrames() >= 10) + if(actor.mStuckFrames >= 10) { - if((physicActor->getLastStuckPosition() - actor.mPosition).length2() < 100) + if((actor.mLastStuckPosition - actor.mPosition).length2() < 100) return; else { - physicActor->setStuckFrames(0); - physicActor->setLastStuckPosition({0, 0, 0}); + actor.mStuckFrames = 0; + actor.mLastStuckPosition = {0, 0, 0}; } } // use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver) // if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation() - const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z()); + const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, actor.mHalfExtentsZ); // use a 3d approximation of the movement vector to better judge player intent - auto velocity = (osg::Quat(actor.mRefpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRefpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement; + auto velocity = (osg::Quat(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; // try to pop outside of the world before doing anything else if we're inside of it - if (!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity += physicActor->getInertialForce(); + if (!actor.mIsOnGround || actor.mIsOnSlope) + velocity += actor.mInertia; // because of the internal collision box offset hack, and the fact that we're moving the collision box manually, // we need to replicate part of the collision box's transform process from scratch osg::Vec3f refPosition = tempPosition + verticalHalfExtent; osg::Vec3f goodPosition = refPosition; - const btTransform oldTransform = collisionObject->getWorldTransform(); + const btTransform oldTransform = actor.mCollisionObject->getWorldTransform(); btTransform newTransform = oldTransform; auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback { goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); - collisionObject->setWorldTransform(newTransform); + actor.mCollisionObject->setWorldTransform(newTransform); - ContactCollectionCallback callback{collisionObject, velocity}; - ContactTestWrapper::contactTest(const_cast(collisionWorld), collisionObject, callback); + ContactCollectionCallback callback{actor.mCollisionObject, velocity}; + ContactTestWrapper::contactTest(const_cast(collisionWorld), actor.mCollisionObject, callback); return callback; }; @@ -486,8 +455,8 @@ namespace MWPhysics auto contactCallback = gatherContacts({0.0, 0.0, 0.0}); if(contactCallback.mDistance < -sAllowedPenetration) { - physicActor->setStuckFrames(physicActor->getStuckFrames() + 1); - physicActor->setLastStuckPosition(actor.mPosition); + ++actor.mStuckFrames; + actor.mLastStuckPosition = actor.mPosition; // we are; try moving it out of the world auto positionDelta = contactCallback.mContactSum; // limit rejection delta to the largest known individual rejections @@ -522,11 +491,11 @@ namespace MWPhysics } else { - physicActor->setStuckFrames(0); - physicActor->setLastStuckPosition({0, 0, 0}); + actor.mStuckFrames = 0; + actor.mLastStuckPosition = {0, 0, 0}; } - collisionObject->setWorldTransform(oldTransform); + actor.mCollisionObject->setWorldTransform(oldTransform); actor.mPosition = tempPosition; } } diff --git a/apps/openmw/mwphysics/mtphysics.cpp b/apps/openmw/mwphysics/mtphysics.cpp index 9b98e7e8f7..7363b9964a 100644 --- a/apps/openmw/mwphysics/mtphysics.cpp +++ b/apps/openmw/mwphysics/mtphysics.cpp @@ -11,7 +11,6 @@ #include "../mwmechanics/movement.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" #include "actor.hpp" #include "contacttestwrapper.h" @@ -50,60 +49,38 @@ namespace bool mCanBeSharedLock; }; + bool isUnderWater(const MWPhysics::ActorFrameData& actorData) + { + return actorData.mPosition.z() < actorData.mSwimLevel; + } + void handleFall(MWPhysics::ActorFrameData& actorData, bool simulationPerformed) { const float heightDiff = actorData.mPosition.z() - actorData.mOldHeight; - const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mActorRaw->getOnGround()); + const bool isStillOnGround = (simulationPerformed && actorData.mWasOnGround && actorData.mIsOnGround); - if (isStillOnGround || actorData.mFlying || actorData.mSwimming || actorData.mSlowFall < 1) + if (isStillOnGround || actorData.mFlying || isUnderWater(actorData) || actorData.mSlowFall < 1) actorData.mNeedLand = true; else if (heightDiff < 0) actorData.mFallHeight += heightDiff; } - void handleJump(const MWWorld::Ptr &ptr) + void updateMechanics(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData) { - const bool isPlayer = (ptr == MWMechanics::getPlayer()); - // Advance acrobatics and set flag for GetPCJumping - if (isPlayer) - { - ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); - MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); - } - - // Decrease fatigue - if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) - { - const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); - const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); - const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); - const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); - const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; - MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); - } - ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; - } - - void updateMechanics(MWPhysics::ActorFrameData& actorData) - { - auto ptr = actorData.mActorRaw->getPtr(); - if (actorData.mDidJump) - handleJump(ptr); + auto ptr = actor.getPtr(); MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (actorData.mNeedLand) - stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || actorData.mSwimming)); + stats.land(ptr == MWMechanics::getPlayer() && (actorData.mFlying || isUnderWater(actorData))); else if (actorData.mFallHeight < 0) stats.addToFallHeight(-actorData.mFallHeight); } - osg::Vec3f interpolateMovements(MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) + osg::Vec3f interpolateMovements(MWPhysics::Actor& actor, MWPhysics::ActorFrameData& actorData, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); - return actorData.mPosition * interpolationFactor + actorData.mActorRaw->getPreviousPosition() * (1.f - interpolationFactor); + return actorData.mPosition * interpolationFactor + actor.getPreviousPosition() * (1.f - interpolationFactor); } namespace Config @@ -137,7 +114,6 @@ namespace MWPhysics , mNumJobs(0) , mRemainingSteps(0) , mLOSCacheExpiry(Settings::Manager::getInt("lineofsight keep inactive cache", "Physics")) - , mDeferAabbUpdate(Settings::Manager::getBool("defer aabb update", "Physics")) , mNewFrame(false) , mAdvanceSimulation(false) , mQuit(false) @@ -163,8 +139,7 @@ namespace MWPhysics } else { - mLOSCacheExpiry = -1; - mDeferAabbUpdate = false; + mLOSCacheExpiry = 0; } mPreStepBarrier = std::make_unique(mNumThreads); @@ -232,38 +207,23 @@ namespace MWPhysics return std::make_tuple(numSteps, actualDelta); } - const std::vector& PhysicsTaskScheduler::moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + void PhysicsTaskScheduler::applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { // This function run in the main thread. // While the mSimulationMutex is held, background physics threads can't run. std::unique_lock lock(mSimulationMutex); + assert(actors.size() == actorsData.size()); double timeStart = mTimer->tick(); - mMovedActors.clear(); - // start by finishing previous background computation if (mNumThreads != 0) { - for (auto& data : mActorsFrameData) + for (size_t i = 0; i < mActors.size(); ++i) { - const auto actorActive = [&data](const auto& newFrameData) -> bool - { - const auto actor = data.mActor.lock(); - return actor && actor->getPtr() == newFrameData.mActorRaw->getPtr(); - }; - // Only return actors that are still part of the scene - if (std::any_of(actorsData.begin(), actorsData.end(), actorActive)) - { - updateMechanics(data); - - // these variables are accessed directly from the main thread, update them here to prevent accessing "too new" values - if (mAdvanceSimulation) - data.mActorRaw->setStandingOnPtr(data.mStandingOn); - data.mActorRaw->setSimulationPosition(interpolateMovements(data, mTimeAccum, mPhysicsDt)); - mMovedActors.emplace_back(data.mActorRaw->getPtr()); - } + updateMechanics(*mActors[i], mActorsFrameData[i]); + updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); } if(mAdvanceSimulation) mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); @@ -274,12 +234,15 @@ namespace MWPhysics timeAccum -= numSteps*newDelta; // init - for (auto& data : actorsData) - data.updatePosition(mCollisionWorld); + for (size_t i = 0; i < actors.size(); ++i) + { + actorsData[i].updatePosition(*actors[i], mCollisionWorld); + } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; + mActors = std::move(actors); mActorsFrameData = std::move(actorsData); mAdvanceSimulation = (mRemainingSteps != 0); mNewFrame = true; @@ -298,7 +261,7 @@ namespace MWPhysics syncComputation(); if(mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); - return mMovedActors; + return; } mAsyncStartTime = mTimer->tick(); @@ -306,23 +269,20 @@ namespace MWPhysics mHasJob.notify_all(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); - return mMovedActors; } - const std::vector& PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) + void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { std::unique_lock lock(mSimulationMutex); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); - mMovedActors.clear(); + mActors.clear(); mActorsFrameData.clear(); for (const auto& [_, actor] : actors) { actor->updatePosition(); actor->updateCollisionObjectPosition(); - mMovedActors.emplace_back(actor->getPtr()); } - return mMovedActors; } void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const @@ -380,19 +340,21 @@ namespace MWPhysics void PhysicsTaskScheduler::addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { + mCollisionObjects.insert(collisionObject); std::unique_lock lock(mCollisionWorldMutex); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { + mCollisionObjects.erase(collisionObject); std::unique_lock lock(mCollisionWorldMutex); mCollisionWorld->removeCollisionObject(collisionObject); } - void PhysicsTaskScheduler::updateSingleAabb(std::weak_ptr ptr, bool immediate) + void PhysicsTaskScheduler::updateSingleAabb(std::shared_ptr ptr, bool immediate) { - if (!mDeferAabbUpdate || immediate) + if (immediate || mNumThreads == 0) { updatePtrAabb(ptr); } @@ -403,22 +365,16 @@ namespace MWPhysics } } - bool PhysicsTaskScheduler::getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2) + bool PhysicsTaskScheduler::getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2) { std::unique_lock lock(mLOSCacheMutex); - auto actorPtr1 = actor1.lock(); - auto actorPtr2 = actor2.lock(); - if (!actorPtr1 || !actorPtr2) - return false; - auto req = LOSRequest(actor1, actor2); auto result = std::find(mLOSCache.begin(), mLOSCache.end(), req); if (result == mLOSCache.end()) { - req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); - if (mLOSCacheExpiry >= 0) - mLOSCache.push_back(req); + req.mResult = hasLineOfSight(actor1.get(), actor2.get()); + mLOSCache.push_back(req); return req.mResult; } result->mAge = 0; @@ -448,31 +404,28 @@ namespace MWPhysics { std::scoped_lock lock(mUpdateAabbMutex); std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(), - [this](const std::weak_ptr& ptr) { updatePtrAabb(ptr); }); + [this](const std::shared_ptr& ptr) { updatePtrAabb(ptr); }); mUpdateAabb.clear(); } - void PhysicsTaskScheduler::updatePtrAabb(const std::weak_ptr& ptr) + void PhysicsTaskScheduler::updatePtrAabb(const std::shared_ptr& ptr) { - if (const auto p = ptr.lock()) + std::scoped_lock lock(mCollisionWorldMutex); + if (const auto actor = std::dynamic_pointer_cast(ptr)) { - std::scoped_lock lock(mCollisionWorldMutex); - if (const auto actor = std::dynamic_pointer_cast(p)) - { - actor->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } - else if (const auto object = std::dynamic_pointer_cast(p)) - { - object->commitPositionChange(); - mCollisionWorld->updateSingleAabb(object->getCollisionObject()); - } - else if (const auto projectile = std::dynamic_pointer_cast(p)) - { - projectile->commitPositionChange(); - mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); - } - }; + actor->updateCollisionObjectPosition(); + mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); + } + else if (const auto object = std::dynamic_pointer_cast(ptr)) + { + object->commitPositionChange(); + mCollisionWorld->updateSingleAabb(object->getCollisionObject()); + } + else if (const auto projectile = std::dynamic_pointer_cast(ptr)) + { + projectile->commitPositionChange(); + mCollisionWorld->updateSingleAabb(projectile->getCollisionObject()); + } } void PhysicsTaskScheduler::worker() @@ -488,11 +441,8 @@ namespace MWPhysics int job = 0; while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { - if(const auto actor = mActorsFrameData[job].mActor.lock()) - { - MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); - MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); - } + MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet); + MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld, *mWorldFrameData); } mPostStepBarrier->wait([this] { afterPostStep(); }); @@ -501,15 +451,10 @@ namespace MWPhysics { while ((job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs) { - if(const auto actor = mActorsFrameData[job].mActor.lock()) - { - auto& actorData = mActorsFrameData[job]; - handleFall(actorData, mAdvanceSimulation); - } + handleFall(mActorsFrameData[job], mAdvanceSimulation); } - if (mLOSCacheExpiry >= 0) - refreshLOSCache(); + refreshLOSCache(); mPostSimBarrier->wait([this] { afterPostSim(); }); } } @@ -517,21 +462,39 @@ namespace MWPhysics void PhysicsTaskScheduler::updateActorsPositions() { - for (auto& actorData : mActorsFrameData) + for (size_t i = 0; i < mActors.size(); ++i) { - if(const auto actor = actorData.mActor.lock()) + if (mActors[i]->setPosition(mActorsFrameData[i].mPosition)) { - if (actor->setPosition(actorData.mPosition)) - { - std::scoped_lock lock(mCollisionWorldMutex); - actorData.mPosition = actor->getPosition(); // account for potential position change made by script - actor->updateCollisionObjectPosition(); - mCollisionWorld->updateSingleAabb(actor->getCollisionObject()); - } + std::scoped_lock lock(mCollisionWorldMutex); + 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 + { + actor.setSimulationPosition(interpolateMovements(actor, actorData, timeAccum, dt)); + actor.setLastStuckPosition(actorData.mLastStuckPosition); + actor.setStuckFrames(actorData.mStuckFrames); + if (simulationPerformed) + { + 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); + } + } + bool PhysicsTaskScheduler::hasLineOfSight(const Actor* actor1, const Actor* actor2) { btVector3 pos1 = Misc::Convert::toBullet(actor1->getCollisionObjectPosition() + osg::Vec3f(0,0,actor1->getHalfExtents().z() * 0.9)); // eye level @@ -560,15 +523,13 @@ namespace MWPhysics updateActorsPositions(); } - for (auto& actorData : mActorsFrameData) + for (size_t i = 0; i < mActors.size(); ++i) { - handleFall(actorData, mAdvanceSimulation); - actorData.mActorRaw->setSimulationPosition(interpolateMovements(actorData, mTimeAccum, mPhysicsDt)); - updateMechanics(actorData); - mMovedActors.emplace_back(actorData.mActorRaw->getPtr()); - if (mAdvanceSimulation) - actorData.mActorRaw->setStandingOnPtr(actorData.mStandingOn); + handleFall(mActorsFrameData[i], mAdvanceSimulation); + updateMechanics(*mActors[i], mActorsFrameData[i]); + updateActor(*mActors[i], mActorsFrameData[i], mAdvanceSimulation, mTimeAccum, mPhysicsDt); } + refreshLOSCache(); } void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) @@ -592,18 +553,31 @@ namespace MWPhysics mDebugDrawer->step(); } + void* PhysicsTaskScheduler::getUserPointer(const btCollisionObject* object) const + { + auto it = mCollisionObjects.find(object); + if (it == mCollisionObjects.end()) + return nullptr; + return (*it)->getUserPointer(); + } + + void PhysicsTaskScheduler::releaseSharedStates() + { + std::scoped_lock lock(mSimulationMutex, mUpdateAabbMutex); + mActors.clear(); + mUpdateAabb.clear(); + } + void PhysicsTaskScheduler::afterPreStep() { - if (mDeferAabbUpdate) - updateAabbs(); + updateAabbs(); if (!mRemainingSteps) return; - for (auto& data : mActorsFrameData) - if (data.mActor.lock()) - { - std::unique_lock lock(mCollisionWorldMutex); - MovementSolver::unstuck(data, mCollisionWorld); - } + for (size_t i = 0; i < mActors.size(); ++i) + { + std::unique_lock lock(mCollisionWorldMutex); + MovementSolver::unstuck(mActorsFrameData[i], mCollisionWorld); + } } void PhysicsTaskScheduler::afterPostStep() @@ -619,7 +593,6 @@ namespace MWPhysics void PhysicsTaskScheduler::afterPostSim() { mNewFrame = false; - if (mLOSCacheExpiry >= 0) { std::unique_lock lock(mLOSCacheMutex); mLOSCache.erase( diff --git a/apps/openmw/mwphysics/mtphysics.hpp b/apps/openmw/mwphysics/mtphysics.hpp index 3fd4d5a693..e2cfccffb0 100644 --- a/apps/openmw/mwphysics/mtphysics.hpp +++ b/apps/openmw/mwphysics/mtphysics.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -38,9 +39,9 @@ namespace MWPhysics /// @param timeAccum accumulated time from previous run to interpolate movements /// @param actorsData per actor data needed to compute new positions /// @return new position of each actor - const std::vector& moveActors(float & timeAccum, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + void applyQueuedMovements(float & timeAccum, std::vector>&& actors, std::vector&& actorsData, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - const std::vector& resetSimulation(const ActorMap& actors); + void resetSimulation(const ActorMap& actors); // Thread safe wrappers void rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const; @@ -52,18 +53,22 @@ namespace MWPhysics void setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask); void addCollisionObject(btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask); void removeCollisionObject(btCollisionObject* collisionObject); - void updateSingleAabb(std::weak_ptr ptr, bool immediate=false); - bool getLineOfSight(const std::weak_ptr& actor1, const std::weak_ptr& actor2); + void updateSingleAabb(std::shared_ptr ptr, bool immediate=false); + bool getLineOfSight(const std::shared_ptr& actor1, const std::shared_ptr& actor2); void debugDraw(); + void* getUserPointer(const btCollisionObject* object) const; + + void releaseSharedStates(); // destroy all objects whose destructor can't be safely called from ~PhysicsTaskScheduler() private: void syncComputation(); void 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(); - void updatePtrAabb(const std::weak_ptr& ptr); + void updatePtrAabb(const std::shared_ptr& ptr); void updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); std::tuple calculateStepConfig(float timeAccum) const; void afterPreStep(); @@ -71,15 +76,16 @@ namespace MWPhysics void afterPostSim(); std::unique_ptr mWorldFrameData; + std::vector> mActors; std::vector mActorsFrameData; - std::vector mMovedActors; + std::unordered_set mCollisionObjects; float mDefaultPhysicsDt; float mPhysicsDt; float mTimeAccum; btCollisionWorld* mCollisionWorld; MWRender::DebugDrawer* mDebugDrawer; std::vector mLOSCache; - std::set, std::owner_less>> mUpdateAabb; + std::set> mUpdateAabb; // TODO: use std::experimental::flex_barrier or std::barrier once it becomes a thing std::unique_ptr mPreStepBarrier; @@ -90,7 +96,6 @@ namespace MWPhysics int mNumJobs; int mRemainingSteps; int mLOSCacheExpiry; - bool mDeferAabbUpdate; bool mNewFrame; bool mAdvanceSimulation; bool mThreadSafeBullet; diff --git a/apps/openmw/mwphysics/object.cpp b/apps/openmw/mwphysics/object.cpp index 2a94d28f12..a95672f8cf 100644 --- a/apps/openmw/mwphysics/object.cpp +++ b/apps/openmw/mwphysics/object.cpp @@ -28,7 +28,7 @@ namespace MWPhysics setScale(ptr.getCellRef().getScale()); setRotation(rotation); - setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3())); + updatePosition(); commitPositionChange(); mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile); @@ -54,14 +54,14 @@ namespace MWPhysics void Object::setRotation(osg::Quat quat) { std::unique_lock lock(mPositionMutex); - mLocalTransform.setRotation(Misc::Convert::toBullet(quat)); + mRotation = quat; mTransformUpdatePending = true; } - void Object::setOrigin(const btVector3& vec) + void Object::updatePosition() { std::unique_lock lock(mPositionMutex); - mLocalTransform.setOrigin(vec); + mPosition = mPtr.getRefData().getPosition().asVec3(); mTransformUpdatePending = true; } @@ -75,25 +75,21 @@ namespace MWPhysics } if (mTransformUpdatePending) { - mCollisionObject->setWorldTransform(mLocalTransform); + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } - btCollisionObject* Object::getCollisionObject() - { - return mCollisionObject.get(); - } - - const btCollisionObject* Object::getCollisionObject() const - { - return mCollisionObject.get(); - } - btTransform Object::getTransform() const { std::unique_lock lock(mPositionMutex); - return mLocalTransform; + btTransform trans; + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + trans.setRotation(Misc::Convert::toBullet(mRotation)); + return trans; } bool Object::isSolid() const diff --git a/apps/openmw/mwphysics/object.hpp b/apps/openmw/mwphysics/object.hpp index c2273831e5..1484c1472c 100644 --- a/apps/openmw/mwphysics/object.hpp +++ b/apps/openmw/mwphysics/object.hpp @@ -16,7 +16,6 @@ namespace Resource } class btCollisionObject; -class btQuaternion; class btVector3; namespace MWPhysics @@ -32,10 +31,8 @@ namespace MWPhysics const Resource::BulletShapeInstance* getShapeInstance() const; void setScale(float scale); void setRotation(osg::Quat quat); - void setOrigin(const btVector3& vec); + void updatePosition(); void commitPositionChange(); - btCollisionObject* getCollisionObject(); - const btCollisionObject* getCollisionObject() const; btTransform getTransform() const; /// Return solid flag. Not used by the object itself, true by default. bool isSolid() const; @@ -46,12 +43,12 @@ namespace MWPhysics bool animateCollisionShapes(); private: - std::unique_ptr mCollisionObject; osg::ref_ptr mShapeInstance; std::map mRecIndexToNodePath; bool mSolid; btVector3 mScale; - btTransform mLocalTransform; + osg::Vec3f mPosition; + osg::Quat mRotation; bool mScaleUpdatePending; bool mTransformUpdatePending; mutable std::mutex mPositionMutex; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index e5bd936abb..a8c39efee2 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -38,6 +38,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/player.hpp" #include "../mwrender/bulletdebugdraw.hpp" @@ -72,6 +73,36 @@ namespace tracer.doTrace(physicActor->getCollisionObject(), startingPosition, destinationPosition, world); return (tracer.mFraction >= 1.0f); } + + void handleJump(const MWWorld::Ptr &ptr) + { + if (!ptr.getClass().isActor()) + return; + if (ptr.getClass().getMovementSettings(ptr).mPosition[2] == 0) + return; + const bool isPlayer = (ptr == MWMechanics::getPlayer()); + // Advance acrobatics and set flag for GetPCJumping + if (isPlayer) + { + ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 0); + MWBase::Environment::get().getWorld()->getPlayer().setJumping(true); + } + + // Decrease fatigue + if (!isPlayer || !MWBase::Environment::get().getWorld()->getGodModeState()) + { + const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); + const float fFatigueJumpBase = gmst.find("fFatigueJumpBase")->mValue.getFloat(); + const float fFatigueJumpMult = gmst.find("fFatigueJumpMult")->mValue.getFloat(); + const float normalizedEncumbrance = std::min(1.f, ptr.getClass().getNormalizedEncumbrance(ptr)); + const float fatigueDecrease = fFatigueJumpBase + normalizedEncumbrance * fFatigueJumpMult; + MWMechanics::DynamicStat fatigue = ptr.getClass().getCreatureStats(ptr).getFatigue(); + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + ptr.getClass().getCreatureStats(ptr).setFatigue(fatigue); + } + ptr.getClass().getMovementSettings(ptr).mPosition[2] = 0; + } + } namespace MWPhysics @@ -122,6 +153,7 @@ namespace MWPhysics if (mWaterCollisionObject) mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); + mTaskScheduler->releaseSharedStates(); mHeightFields.clear(); mObjects.clear(); mActors.clear(); @@ -149,7 +181,7 @@ namespace MWPhysics void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); + ObjectMap::iterator found = mObjects.find(ptr.mRef); if (found == mObjects.end()) return; @@ -166,7 +198,7 @@ namespace MWPhysics if (obj.isEmpty()) return true; // assume standing on terrain (which is a non-object, so not collision tracked) - ObjectMap::const_iterator foundObj = mObjects.find(obj); + ObjectMap::const_iterator foundObj = mObjects.find(obj.mRef); if (foundObj == mObjects.end()) return false; @@ -316,11 +348,11 @@ namespace MWPhysics return result; } - RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius) const + RayCastingResult PhysicsSystem::castSphere(const osg::Vec3f &from, const osg::Vec3f &to, float radius, int mask, int group) const { btCollisionWorld::ClosestConvexResultCallback callback(Misc::Convert::toBullet(from), Misc::Convert::toBullet(to)); - callback.m_collisionFilterGroup = 0xff; - callback.m_collisionFilterMask = CollisionType_World|CollisionType_HeightMap|CollisionType_Door; + callback.m_collisionFilterGroup = group; + callback.m_collisionFilterMask = mask; btSphereShape shape(radius); const btQuaternion btrot = btQuaternion::getIdentity(); @@ -336,21 +368,20 @@ namespace MWPhysics { result.mHitPos = Misc::Convert::toOsg(callback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(callback.m_hitNormalWorld); + if (auto* ptrHolder = static_cast(callback.m_hitCollisionObject->getUserPointer())) + result.mHitObject = ptrHolder->getPtr(); } return result; } bool PhysicsSystem::getLineOfSight(const MWWorld::ConstPtr &actor1, const MWWorld::ConstPtr &actor2) const { - const auto getWeakPtr = [&](const MWWorld::ConstPtr &ptr) -> std::weak_ptr - { - const auto found = mActors.find(ptr); - if (found != mActors.end()) - return { found->second }; - return {}; - }; + const auto it1 = mActors.find(actor1.mRef); + const auto it2 = mActors.find(actor2.mRef); + if (it1 == mActors.end() || it2 == mActors.end()) + return false; - return mTaskScheduler->getLineOfSight(getWeakPtr(actor1), getWeakPtr(actor2)); + return mTaskScheduler->getLineOfSight(it1->second, it2->second); } bool PhysicsSystem::isOnGround(const MWWorld::Ptr &actor) @@ -412,7 +443,7 @@ namespace MWPhysics { btCollisionObject* me = nullptr; - auto found = mObjects.find(ptr); + auto found = mObjects.find(ptr.mRef); if (found != mObjects.end()) me = found->second->getCollisionObject(); else @@ -435,7 +466,7 @@ namespace MWPhysics osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, float maxHeight) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found == mActors.end()) return ptr.getRefData().getPosition().asVec3(); return MovementSolver::traceDown(ptr, position, found->second.get(), mCollisionWorld.get(), maxHeight); @@ -461,7 +492,7 @@ namespace MWPhysics return heightField->second.get(); } - void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType, bool skipAnimated) + void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType) { if (ptr.mRef->mData.mPhysicsPostponed) return; @@ -469,13 +500,10 @@ namespace MWPhysics if (!shapeInstance || !shapeInstance->getCollisionShape()) return; - if (skipAnimated && shapeInstance->isAnimated()) - return; - assert(!getObject(ptr)); auto obj = std::make_shared(ptr, shapeInstance, rotation, collisionType, mTaskScheduler.get()); - mObjects.emplace(ptr, obj); + mObjects.emplace(ptr.mRef, obj); if (obj->isAnimated()) mAnimatedObjects.insert(obj.get()); @@ -483,19 +511,16 @@ namespace MWPhysics void PhysicsSystem::remove(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { if (mUnrefQueue.get()) - mUnrefQueue->push(found->second->getShapeInstance()); + mUnrefQueue->push(foundObject->second->getShapeInstance()); - mAnimatedObjects.erase(found->second.get()); + mAnimatedObjects.erase(foundObject->second.get()); - mObjects.erase(found); + mObjects.erase(foundObject); } - - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { mActors.erase(foundActor); } @@ -510,23 +535,10 @@ namespace MWPhysics void PhysicsSystem::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated) { - ObjectMap::iterator found = mObjects.find(old); - if (found != mObjects.end()) - { - auto obj = found->second; - obj->updatePtr(updated); - mObjects.erase(found); - mObjects.emplace(updated, std::move(obj)); - } - - ActorMap::iterator foundActor = mActors.find(old); - if (foundActor != mActors.end()) - { - auto actor = foundActor->second; - actor->updatePtr(updated); - mActors.erase(foundActor); - mActors.emplace(updated, std::move(actor)); - } + 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); for (auto& [_, actor] : mActors) { @@ -544,7 +556,7 @@ namespace MWPhysics Actor *PhysicsSystem::getActor(const MWWorld::Ptr &ptr) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; @@ -552,7 +564,7 @@ namespace MWPhysics const Actor *PhysicsSystem::getActor(const MWWorld::ConstPtr &ptr) const { - ActorMap::const_iterator found = mActors.find(ptr); + ActorMap::const_iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) return found->second.get(); return nullptr; @@ -560,7 +572,7 @@ namespace MWPhysics const Object* PhysicsSystem::getObject(const MWWorld::ConstPtr &ptr) const { - ObjectMap::const_iterator found = mObjects.find(ptr); + ObjectMap::const_iterator found = mObjects.find(ptr.mRef); if (found != mObjects.end()) return found->second.get(); return nullptr; @@ -576,20 +588,16 @@ namespace MWPhysics void PhysicsSystem::updateScale(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { float scale = ptr.getCellRef().getScale(); - found->second->setScale(scale); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setScale(scale); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updateScale(); mTaskScheduler->updateSingleAabb(foundActor->second); - return; } } @@ -605,19 +613,7 @@ namespace MWPhysics if (btFrom == btTo) return; - const auto casterPtr = projectile->getCaster(); - const auto* caster = [this,&casterPtr]() -> const btCollisionObject* - { - const Actor* actor = getActor(casterPtr); - if (actor) - return actor->getCollisionObject(); - const Object* object = getObject(casterPtr); - if (object) - return object->getCollisionObject(); - return nullptr; - }(); - - ProjectileConvexCallback resultCallback(caster, btFrom, btTo, projectile); + ProjectileConvexCallback resultCallback(projectile->getCasterCollisionObject(), projectile->getCollisionObject(), btFrom, btTo, projectile); resultCallback.m_collisionFilterMask = 0xff; resultCallback.m_collisionFilterGroup = CollisionType_Projectile; @@ -627,47 +623,39 @@ namespace MWPhysics mTaskScheduler->convexSweepTest(projectile->getConvexShape(), from_, to_, resultCallback); - const auto newpos = projectile->isActive() ? position : Misc::Convert::toOsg(resultCallback.m_hitPointWorld); + 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) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setRotation(rotate); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->setRotation(rotate); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { if (!foundActor->second->isRotationallyInvariant()) { foundActor->second->setRotation(rotate); mTaskScheduler->updateSingleAabb(foundActor->second); } - return; } } void PhysicsSystem::updatePosition(const MWWorld::Ptr &ptr) { - ObjectMap::iterator found = mObjects.find(ptr); - if (found != mObjects.end()) + if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { - found->second->setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3())); - mTaskScheduler->updateSingleAabb(found->second); - return; + foundObject->second->updatePosition(); + mTaskScheduler->updateSingleAabb(foundObject->second); } - ActorMap::iterator foundActor = mActors.find(ptr); - if (foundActor != mActors.end()) + else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updatePosition(); mTaskScheduler->updateSingleAabb(foundActor->second, true); - return; } } @@ -688,11 +676,16 @@ namespace MWPhysics if (!shape) return; - auto actor = std::make_shared(ptr, shape, mTaskScheduler.get()); - mActors.emplace(ptr, std::move(actor)); + // check if Actor should spawn above water + const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(ptr).getMagicEffects(); + const bool canWaterWalk = effects.get(ESM::MagicEffect::WaterWalking).getMagnitude() > 0; + + auto actor = std::make_shared(ptr, shape, mTaskScheduler.get(), canWaterWalk); + + mActors.emplace(ptr.mRef, std::move(actor)); } - int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater) + int PhysicsSystem::addProjectile (const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); @@ -700,7 +693,7 @@ namespace MWPhysics mProjectileId++; - auto projectile = std::make_shared(caster, position, radius, canTraverseWater, mTaskScheduler.get(), this); + auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); mProjectiles.emplace(mProjectileId, std::move(projectile)); return mProjectileId; @@ -717,7 +710,7 @@ namespace MWPhysics bool PhysicsSystem::toggleCollisionMode() { - ActorMap::iterator found = mActors.find(MWMechanics::getPlayer()); + ActorMap::iterator found = mActors.find(MWMechanics::getPlayer().mRef); if (found != mActors.end()) { bool cmode = found->second->getCollisionMode(); @@ -732,7 +725,7 @@ namespace MWPhysics void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity) { - ActorMap::iterator found = mActors.find(ptr); + ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) found->second->setVelocity(velocity); } @@ -743,35 +736,29 @@ namespace MWPhysics actor->setVelocity(osg::Vec3f()); } - const std::vector& PhysicsSystem::applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) + std::pair>, std::vector> PhysicsSystem::prepareFrameData(bool willSimulate) { - mTimeAccum += dt; - - if (skipSimulation) - return mTaskScheduler->resetSimulation(mActors); - - // modifies mTimeAccum - return mTaskScheduler->moveActors(mTimeAccum, prepareFrameData(mTimeAccum >= mPhysicsDt), frameStart, frameNumber, stats); - } - - std::vector PhysicsSystem::prepareFrameData(bool willSimulate) - { - std::vector actorsFrameData; - actorsFrameData.reserve(mActors.size()); + std::pair>, std::vector> framedata; + framedata.first.reserve(mActors.size()); + framedata.second.reserve(mActors.size()); const MWBase::World *world = MWBase::Environment::get().getWorld(); - for (const auto& [ptr, physicActor] : mActors) + for (const auto& [ref, physicActor] : mActors) { + auto ptr = physicActor->getPtr(); + if (!ptr.getClass().isMobile(ptr)) + continue; float waterlevel = -std::numeric_limits::max(); const MWWorld::CellStore *cell = ptr.getCell(); if(cell->getCell()->hasWater()) waterlevel = cell->getWaterLevel(); - const MWMechanics::MagicEffects& effects = ptr.getClass().getCreatureStats(physicActor->getPtr()).getMagicEffects(); + const auto& stats = ptr.getClass().getCreatureStats(ptr); + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); bool waterCollision = false; if (cell->getCell()->hasWater() && effects.get(ESM::MagicEffect::WaterWalking).getMagnitude()) { - if (physicActor->getCollisionMode() || !world->isUnderwater(ptr.getCell(), osg::Vec3f(ptr.getRefData().getPosition().asVec3()))) + if (physicActor->getCollisionMode() || !world->isUnderwater(ptr.getCell(), ptr.getRefData().getPosition().asVec3())) waterCollision = true; } @@ -779,36 +766,72 @@ namespace MWPhysics // 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 bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); + const bool inert = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - // Ue current value only if we don't advance the simulation. Otherwise we might get a stale value. - MWWorld::Ptr standingOn; - if (!willSimulate) - standingOn = physicActor->getStandingOnPtr(); + framedata.first.emplace_back(physicActor); + framedata.second.emplace_back(*physicActor, inert, waterCollision, slowFall, waterlevel); - actorsFrameData.emplace_back(physicActor, standingOn, waterCollision, slowFall, waterlevel); + // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. + if (willSimulate) + handleJump(ptr); } - return actorsFrameData; + return framedata; } - void PhysicsSystem::stepSimulation() + void PhysicsSystem::stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { for (Object* animatedObject : mAnimatedObjects) + { if (animatedObject->animateCollisionShapes()) { - auto obj = mObjects.find(animatedObject->getPtr()); + auto obj = mObjects.find(animatedObject->getPtr().mRef); assert(obj != mObjects.end()); mTaskScheduler->updateSingleAabb(obj->second); } + } #ifndef BT_NO_PROFILE CProfileManager::Reset(); CProfileManager::Increment_Frame_Counter(); #endif + + mTimeAccum += dt; + + if (skipSimulation) + mTaskScheduler->resetSimulation(mActors); + else + { + auto [actors, framedata] = prepareFrameData(mTimeAccum >= mPhysicsDt); + // modifies mTimeAccum + mTaskScheduler->applyQueuedMovements(mTimeAccum, std::move(actors), std::move(framedata), frameStart, frameNumber, stats); + } + } + + void PhysicsSystem::moveActors() + { + auto* player = getActor(MWMechanics::getPlayer()); + auto* world = MWBase::Environment::get().getWorld(); + + // copy new ptr position in temporary vector. player is handled separately as its movement might change active cell. + std::vector> newPositions; + newPositions.reserve(mActors.size() - 1); + for (const auto& [ptr, physicActor] : mActors) + { + if (physicActor.get() == player) + continue; + newPositions.emplace_back(physicActor->getPtr(), physicActor->getSimulationPosition()); + } + + for (auto& [ptr, pos] : newPositions) + world->moveObject(ptr, pos, false, false); + + world->moveObject(player->getPtr(), player->getSimulationPosition(), false, false); } void PhysicsSystem::updateAnimatedCollisionShape(const MWWorld::Ptr& object) { - ObjectMap::iterator found = mObjects.find(object); + ObjectMap::iterator found = mObjects.find(object.mRef); if (found != mObjects.end()) if (found->second->animateCollisionShapes()) mTaskScheduler->updateSingleAabb(found->second); @@ -822,7 +845,7 @@ namespace MWPhysics bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr &actor, const MWWorld::ConstPtr &object) const { - const auto physActor = mActors.find(actor); + const auto physActor = mActors.find(actor.mRef); if (physActor != mActors.end()) return physActor->second->getStandingOnPtr() == object; return false; @@ -897,10 +920,11 @@ namespace MWPhysics CollisionType_Actor|CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const { btCollisionObject* object = nullptr; - const auto it = mActors.find(ignore); + const auto it = mActors.find(ignore.mRef); if (it != mActors.end()) object = it->second->getCollisionObject(); const auto bulletPosition = Misc::Convert::toBullet(position); @@ -908,7 +932,19 @@ namespace MWPhysics const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = 0xff; - HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group); + if (occupyingActors == nullptr) + { + HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group, + static_cast(nullptr)); + mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); + return callback.getResult(); + } + const auto onCollision = [&] (const btCollisionObject* object) + { + if (PtrHolder* holder = static_cast(object->getUserPointer())) + occupyingActors->push_back(holder->getPtr()); + }; + HasSphereCollisionCallback callback(bulletPosition, radius, object, mask, group, &onCollision); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } @@ -917,6 +953,7 @@ namespace MWPhysics { stats.setAttribute(frameNumber, "Physics Actors", mActors.size()); stats.setAttribute(frameNumber, "Physics Objects", mObjects.size()); + stats.setAttribute(frameNumber, "Physics Projectiles", mProjectiles.size()); stats.setAttribute(frameNumber, "Physics HeightFields", mHeightFields.size()); } @@ -926,34 +963,47 @@ namespace MWPhysics mDebugDrawer->addCollision(position, normal); } - ActorFrameData::ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, - bool waterCollision, float slowFall, float waterlevel) - : mActor(actor), mActorRaw(actor.get()), mStandingOn(standingOn), - mDidJump(false), mNeedLand(false), mWaterCollision(waterCollision), mSkipCollisionDetection(actor->skipCollisions()), - mWaterlevel(waterlevel), mSlowFall(slowFall), mOldHeight(0), mFallHeight(0), mMovement(actor->velocity()), mPosition(), mRefpos() + ActorFrameData::ActorFrameData(Actor& actor, bool inert, bool waterCollision, float slowFall, float waterlevel) + : mPosition() + , mStandingOn(nullptr) + , mIsOnGround(actor.getOnGround()) + , mIsOnSlope(actor.getOnSlope()) + , mWalkingOnWater(false) + , mInert(inert) + , mCollisionObject(actor.getCollisionObject()) + , mSwimLevel(waterlevel - (actor.getRenderingHalfExtents().z() * 2 * MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat())) + , mSlowFall(slowFall) + , mRotation() + , mMovement(actor.velocity()) + , mWaterlevel(waterlevel) + , mHalfExtentsZ(actor.getHalfExtents().z()) + , mOldHeight(0) + , mFallHeight(0) + , mStuckFrames(0) + , mFlying(MWBase::Environment::get().getWorld()->isFlying(actor.getPtr())) + , mWasOnGround(actor.getOnGround()) + , mIsAquatic(actor.getPtr().getClass().isPureWaterCreature(actor.getPtr())) + , mWaterCollision(waterCollision) + , mSkipCollisionDetection(actor.skipCollisions() || !actor.getCollisionMode()) + , mNeedLand(false) { - const MWBase::World *world = MWBase::Environment::get().getWorld(); - const auto ptr = actor->getPtr(); - mFlying = world->isFlying(ptr); - mSwimming = world->isSwimming(ptr); - mWantJump = ptr.getClass().getMovementSettings(ptr).mPosition[2] != 0; - auto& stats = ptr.getClass().getCreatureStats(ptr); - const bool godmode = ptr == world->getPlayerConstPtr() && world->getGodModeState(); - mFloatToSurface = stats.isDead() || (!godmode && stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0); - mWasOnGround = actor->getOnGround(); } - void ActorFrameData::updatePosition(btCollisionWorld* world) + void ActorFrameData::updatePosition(Actor& actor, btCollisionWorld* world) { - mActorRaw->applyOffsetChange(); - mPosition = mActorRaw->getPosition(); - if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(mActorRaw, mWaterlevel, world)) + actor.applyOffsetChange(); + mPosition = actor.getPosition(); + if (mWaterCollision && mPosition.z() < mWaterlevel && canMoveToWaterSurface(&actor, mWaterlevel, world)) { mPosition.z() = mWaterlevel; - MWBase::Environment::get().getWorld()->moveObject(mActorRaw->getPtr(), mPosition.x(), mPosition.y(), mPosition.z(), false); + MWBase::Environment::get().getWorld()->moveObject(actor.getPtr(), mPosition, false); } mOldHeight = mPosition.z(); - mRefpos = mActorRaw->getPtr().getRefData().getPosition(); + 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 1eb1fd419a..4fb7a3518f 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -56,7 +57,7 @@ namespace MWPhysics class PhysicsTaskScheduler; class Projectile; - using ActorMap = std::map>; + using ActorMap = std::unordered_map>; struct ContactPoint { @@ -78,27 +79,32 @@ namespace MWPhysics struct ActorFrameData { - ActorFrameData(const std::shared_ptr& actor, const MWWorld::Ptr standingOn, bool moveToWaterSurface, float slowFall, float waterlevel); - void updatePosition(btCollisionWorld* world); - std::weak_ptr mActor; - Actor* mActorRaw; - MWWorld::Ptr mStandingOn; - bool mFlying; - bool mSwimming; - bool mWasOnGround; - bool mWantJump; - bool mDidJump; - bool mFloatToSurface; - bool mNeedLand; - bool mWaterCollision; - bool mSkipCollisionDetection; - float mWaterlevel; - float mSlowFall; + 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; + bool mIsOnGround; + bool mIsOnSlope; + bool mWalkingOnWater; + const bool mInert; + btCollisionObject* mCollisionObject; + const float mSwimLevel; + const float mSlowFall; + osg::Vec2f mRotation; + osg::Vec3f mMovement; + osg::Vec3f mLastStuckPosition; + const float mWaterlevel; + const float mHalfExtentsZ; float mOldHeight; float mFallHeight; - osg::Vec3f mMovement; - osg::Vec3f mPosition; - ESM::Position mRefpos; + unsigned int mStuckFrames; + const bool mFlying; + const bool mWasOnGround; + const bool mIsAquatic; + const bool mWaterCollision; + const bool mSkipCollisionDetection; + bool mNeedLand; }; struct WorldFrameData @@ -122,10 +128,10 @@ namespace MWPhysics void setWaterHeight(float height); void disableWater(); - void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType = CollisionType_World, bool skipAnimated = false); + void addObject (const MWWorld::Ptr& ptr, const std::string& mesh, osg::Quat rotation, int collisionType = CollisionType_World); void addActor (const MWWorld::Ptr& ptr, const std::string& mesh); - int addProjectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, const std::string& mesh, bool computeRadius, bool canTraverseWater); + 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); @@ -154,7 +160,11 @@ namespace MWPhysics bool toggleCollisionMode(); - void stepSimulation(); + /// Determine new position based on all queued movements, then clear the list. + void stepSimulation(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); + + /// Apply new positions to actors + void moveActors(); void debugDraw(); std::vector getCollisions(const MWWorld::ConstPtr &ptr, int collisionGroup, int collisionMask) const; ///< get handles this object collides with @@ -176,9 +186,10 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), const std::vector& targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const override; + int mask = CollisionType_Default, int group=0xff) const override; - RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const override; + RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const override; /// Return true if actor1 can see actor2. bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; @@ -204,12 +215,9 @@ namespace MWPhysics osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr &object) const; /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will - /// be overwritten. Valid until the next call to applyQueuedMovement. + /// be overwritten. Valid until the next call to stepSimulation void queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &velocity); - /// Apply all queued movements, then clear the list. - const std::vector& applyQueuedMovement(float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); - /// Clear the queued movements list without applying. void clearQueuedMovement(); @@ -244,7 +252,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); @@ -253,7 +262,7 @@ namespace MWPhysics void updateWater(); - std::vector prepareFrameData(bool willSimulate); + std::pair>, std::vector> prepareFrameData(bool willSimulate); osg::ref_ptr mUnrefQueue; @@ -266,7 +275,7 @@ namespace MWPhysics std::unique_ptr mShapeManager; Resource::ResourceSystem* mResourceSystem; - using ObjectMap = std::map>; + using ObjectMap = std::unordered_map>; ObjectMap mObjects; std::set mAnimatedObjects; // stores pointers to elements in mObjects diff --git a/apps/openmw/mwphysics/projectile.cpp b/apps/openmw/mwphysics/projectile.cpp index 0a6e9ac123..4efb245149 100644 --- a/apps/openmw/mwphysics/projectile.cpp +++ b/apps/openmw/mwphysics/projectile.cpp @@ -3,25 +3,22 @@ #include #include -#include - #include #include "../mwworld/class.hpp" +#include "actor.hpp" #include "collisiontype.hpp" -#include "memory" #include "mtphysics.hpp" +#include "object.hpp" #include "projectile.hpp" namespace MWPhysics { -Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) - : mCanCrossWaterSurface(canCrossWaterSurface) - , mCrossedWaterSurface(false) +Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem) + : mHitWater(false) , mActive(true) - , mCaster(caster) - , mWaterHitPosition(std::nullopt) + , mHitTarget(nullptr) , mPhysics(physicssystem) , mTaskScheduler(scheduler) { @@ -35,6 +32,7 @@ Projectile::Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, f mCollisionObject->setUserPointer(this); setPosition(position); + setCaster(caster); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; @@ -55,7 +53,9 @@ void Projectile::commitPositionChange() std::scoped_lock lock(mMutex); if (mTransformUpdatePending) { - mCollisionObject->setWorldTransform(mLocalTransform); + auto& trans = mCollisionObject->getWorldTransform(); + trans.setOrigin(Misc::Convert::toBullet(mPosition)); + mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } @@ -63,82 +63,77 @@ void Projectile::commitPositionChange() void Projectile::setPosition(const osg::Vec3f &position) { std::scoped_lock lock(mMutex); - mLocalTransform.setOrigin(Misc::Convert::toBullet(position)); + mPosition = position; mTransformUpdatePending = true; } osg::Vec3f Projectile::getPosition() const { std::scoped_lock lock(mMutex); - return Misc::Convert::toOsg(mLocalTransform.getOrigin()); + return mPosition; } -bool Projectile::canTraverseWater() const +void Projectile::hit(const btCollisionObject* target, btVector3 pos, btVector3 normal) { - return mCanCrossWaterSurface; -} - -void Projectile::hit(const MWWorld::Ptr& target, btVector3 pos, btVector3 normal) -{ - if (!mActive.load(std::memory_order_acquire)) + bool active = true; + if (!mActive.compare_exchange_strong(active, false, std::memory_order_relaxed) || !active) return; - std::scoped_lock lock(mMutex); mHitTarget = target; mHitPosition = pos; mHitNormal = normal; - mActive.store(false, std::memory_order_release); +} + +MWWorld::Ptr Projectile::getTarget() const +{ + assert(!mActive); + auto* target = static_cast(mHitTarget->getUserPointer()); + return target ? target->getPtr() : MWWorld::Ptr(); } MWWorld::Ptr Projectile::getCaster() const { - std::scoped_lock lock(mMutex); return mCaster; } void Projectile::setCaster(const MWWorld::Ptr& caster) { - std::scoped_lock lock(mMutex); mCaster = caster; + mCasterColObj = [this,&caster]() -> const btCollisionObject* + { + const Actor* actor = mPhysics->getActor(caster); + if (actor) + return actor->getCollisionObject(); + const Object* object = mPhysics->getObject(caster); + if (object) + return object->getCollisionObject(); + return nullptr; + }(); } void Projectile::setValidTargets(const std::vector& targets) { std::scoped_lock lock(mMutex); - mValidTargets = targets; + mValidTargets.clear(); + for (const auto& ptr : targets) + { + const auto* physicActor = mPhysics->getActor(ptr); + if (physicActor) + mValidTargets.push_back(physicActor->getCollisionObject()); + } } -bool Projectile::isValidTarget(const MWWorld::Ptr& target) const +bool Projectile::isValidTarget(const btCollisionObject* target) const { + assert(target); std::scoped_lock lock(mMutex); - if (mCaster == target) + if (mCasterColObj == target) return false; - if (target.isEmpty() || mValidTargets.empty()) + if (mValidTargets.empty()) return true; - bool validTarget = false; - for (const auto& targetActor : mValidTargets) - { - if (targetActor == target) - { - validTarget = true; - break; - } - } - return validTarget; -} - -std::optional Projectile::getWaterHitPosition() -{ - return std::exchange(mWaterHitPosition, std::nullopt); -} - -void Projectile::setWaterHitPosition(btVector3 pos) -{ - if (mCrossedWaterSurface) - return; - mCrossedWaterSurface = true; - mWaterHitPosition = pos; + return std::any_of(mValidTargets.begin(), mValidTargets.end(), + [target](const btCollisionObject* actor) { return target == actor; }); } } diff --git a/apps/openmw/mwphysics/projectile.hpp b/apps/openmw/mwphysics/projectile.hpp index 7bcf33b38d..5e4e487c03 100644 --- a/apps/openmw/mwphysics/projectile.hpp +++ b/apps/openmw/mwphysics/projectile.hpp @@ -4,14 +4,14 @@ #include #include #include -#include + +#include #include "ptrholder.hpp" class btCollisionObject; class btCollisionShape; class btConvexShape; -class btVector3; namespace osg { @@ -31,7 +31,7 @@ namespace MWPhysics class Projectile final : public PtrHolder { public: - Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, bool canCrossWaterSurface, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); + Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); ~Projectile() override; btConvexShape* getConvexShape() const { return mConvexShape; } @@ -41,53 +41,56 @@ namespace MWPhysics void setPosition(const osg::Vec3f& position); osg::Vec3f getPosition() const; - btCollisionObject* getCollisionObject() const - { - return mCollisionObject.get(); - } - bool isActive() const { return mActive.load(std::memory_order_acquire); } - MWWorld::Ptr getTarget() const - { - assert(!mActive); - return mHitTarget; - } + MWWorld::Ptr getTarget() const; MWWorld::Ptr getCaster() const; void setCaster(const MWWorld::Ptr& caster); + const btCollisionObject* getCasterCollisionObject() const + { + return mCasterColObj; + } - bool canTraverseWater() const; + void setHitWater() + { + mHitWater = true; + } - void hit(const MWWorld::Ptr& target, btVector3 pos, btVector3 normal); + bool getHitWater() const + { + return mHitWater; + } + + void hit(const btCollisionObject* target, btVector3 pos, btVector3 normal); void setValidTargets(const std::vector& targets); - bool isValidTarget(const MWWorld::Ptr& target) const; + bool isValidTarget(const btCollisionObject* target) const; - std::optional getWaterHitPosition(); - void setWaterHitPosition(btVector3 pos); + btVector3 getHitPosition() const + { + return mHitPosition; + } private: std::unique_ptr mShape; btConvexShape* mConvexShape; - std::unique_ptr mCollisionObject; - btTransform mLocalTransform; bool mTransformUpdatePending; - bool mCanCrossWaterSurface; - bool mCrossedWaterSurface; + bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; - MWWorld::Ptr mHitTarget; - std::optional mWaterHitPosition; + const btCollisionObject* mCasterColObj; + const btCollisionObject* mHitTarget; + osg::Vec3f mPosition; btVector3 mHitPosition; btVector3 mHitNormal; - std::vector mValidTargets; + std::vector mValidTargets; mutable std::mutex mMutex; diff --git a/apps/openmw/mwphysics/projectileconvexcallback.cpp b/apps/openmw/mwphysics/projectileconvexcallback.cpp index b803c4400b..6520be787d 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.cpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.cpp @@ -1,3 +1,5 @@ +#include + #include "../mwworld/class.hpp" #include "actor.hpp" @@ -8,59 +10,50 @@ namespace MWPhysics { - ProjectileConvexCallback::ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj) + ProjectileConvexCallback::ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj) : btCollisionWorld::ClosestConvexResultCallback(from, to) - , mMe(me), mProjectile(proj) + , mCaster(caster) + , mMe(me) + , mProjectile(proj) { assert(mProjectile); } btScalar ProjectileConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) { + const auto* hitObject = result.m_hitCollisionObject; // don't hit the caster - if (result.m_hitCollisionObject == mMe) + if (hitObject == mCaster) return 1.f; // don't hit the projectile - if (result.m_hitCollisionObject == mProjectile->getCollisionObject()) + if (hitObject == mMe) return 1.f; btCollisionWorld::ClosestConvexResultCallback::addSingleResult(result, normalInWorldSpace); - switch (result.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup) + switch (hitObject->getBroadphaseHandle()->m_collisionFilterGroup) { case CollisionType_Actor: { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - if (!mProjectile->isValidTarget(target->getPtr())) + if (!mProjectile->isValidTarget(hitObject)) return 1.f; - mProjectile->hit(target->getPtr(), result.m_hitPointLocal, result.m_hitNormalLocal); break; } case CollisionType_Projectile: { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - if (!mProjectile->isValidTarget(target->getCaster())) + auto* target = static_cast(hitObject->getUserPointer()); + if (!mProjectile->isValidTarget(target->getCasterCollisionObject())) return 1.f; - target->hit(mProjectile->getPtr(), m_hitPointWorld, m_hitNormalWorld); - mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld); + target->hit(mMe, m_hitPointWorld, m_hitNormalWorld); break; } case CollisionType_Water: { - mProjectile->setWaterHitPosition(m_hitPointWorld); - if (mProjectile->canTraverseWater()) - return 1.f; - mProjectile->hit(MWWorld::Ptr(), m_hitPointWorld, m_hitNormalWorld); - break; - } - default: - { - auto* target = static_cast(result.m_hitCollisionObject->getUserPointer()); - auto ptr = target ? target->getPtr() : MWWorld::Ptr(); - mProjectile->hit(ptr, m_hitPointWorld, m_hitNormalWorld); + mProjectile->setHitWater(); break; } } + mProjectile->hit(hitObject, m_hitPointWorld, m_hitNormalWorld); return result.m_hitFraction; } diff --git a/apps/openmw/mwphysics/projectileconvexcallback.hpp b/apps/openmw/mwphysics/projectileconvexcallback.hpp index 96c84b1fe2..f35cfbd3c8 100644 --- a/apps/openmw/mwphysics/projectileconvexcallback.hpp +++ b/apps/openmw/mwphysics/projectileconvexcallback.hpp @@ -12,11 +12,12 @@ namespace MWPhysics class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: - ProjectileConvexCallback(const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj); + ProjectileConvexCallback(const btCollisionObject* caster, const btCollisionObject* me, const btVector3& from, const btVector3& to, Projectile* proj); btScalar addSingleResult(btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) override; private: + const btCollisionObject* mCaster; const btCollisionObject* mMe; Projectile* mProjectile; }; diff --git a/apps/openmw/mwphysics/ptrholder.hpp b/apps/openmw/mwphysics/ptrholder.hpp index 152a5d64fc..e84f3d1cfe 100644 --- a/apps/openmw/mwphysics/ptrholder.hpp +++ b/apps/openmw/mwphysics/ptrholder.hpp @@ -2,6 +2,9 @@ #define OPENMW_MWPHYSICS_PTRHOLDER_H #include +#include + +#include #include "../mwworld/ptr.hpp" @@ -10,31 +13,26 @@ namespace MWPhysics class PtrHolder { public: - virtual ~PtrHolder() {} + virtual ~PtrHolder() = default; void updatePtr(const MWWorld::Ptr& updated) { - std::scoped_lock lock(mMutex); mPtr = updated; } MWWorld::Ptr getPtr() { - std::scoped_lock lock(mMutex); return mPtr; } - MWWorld::ConstPtr getPtr() const + btCollisionObject* getCollisionObject() const { - std::scoped_lock lock(mMutex); - return mPtr; + return mCollisionObject.get(); } protected: MWWorld::Ptr mPtr; - - private: - mutable std::mutex mMutex; + std::unique_ptr mCollisionObject; }; } diff --git a/apps/openmw/mwphysics/raycasting.hpp b/apps/openmw/mwphysics/raycasting.hpp index e13e745fec..d00f23e2c4 100644 --- a/apps/openmw/mwphysics/raycasting.hpp +++ b/apps/openmw/mwphysics/raycasting.hpp @@ -29,9 +29,10 @@ namespace MWPhysics /// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all other actors. virtual RayCastingResult castRay(const osg::Vec3f &from, const osg::Vec3f &to, const MWWorld::ConstPtr& ignore = MWWorld::ConstPtr(), const std::vector& targets = std::vector(), - int mask = CollisionType_World|CollisionType_HeightMap|CollisionType_Actor|CollisionType_Door, int group=0xff) const = 0; + int mask = CollisionType_Default, int group=0xff) const = 0; - virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius) const = 0; + virtual RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, + int mask = CollisionType_Default, int group=0xff) const = 0; /// Return true if actor1 can see actor2. virtual bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const = 0; diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index 59bc327656..f556e6891a 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -84,7 +84,7 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } -std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +std::string ActorAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const { std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; @@ -271,7 +271,7 @@ bool ActorAnimation::useShieldAnimations() const return false; } -osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) +osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) const { if (!mObjectRoot) return nullptr; diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index e149e44148..61ad1ca235 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -41,11 +41,11 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener bool updateCarriedLeftVisible(const int weaptype) const override; protected: - osg::Group* getBoneByName(const std::string& boneName); + osg::Group* getBoneByName(const std::string& boneName) const; virtual void updateHolsteredWeapon(bool showHolsteredWeapons); virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); - virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; + virtual std::string getShieldMesh(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) diff --git a/apps/openmw/mwrender/actorspaths.cpp b/apps/openmw/mwrender/actorspaths.cpp index 35b2553555..4e3bfd79a6 100644 --- a/apps/openmw/mwrender/actorspaths.cpp +++ b/apps/openmw/mwrender/actorspaths.cpp @@ -2,9 +2,13 @@ #include "vismask.hpp" #include +#include +#include #include +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" namespace MWRender { ActorsPaths::ActorsPaths(const osg::ref_ptr& root, bool enabled) @@ -43,6 +47,7 @@ namespace MWRender const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings); if (newGroup) { + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(newGroup, "debug"); newGroup->setNodeMask(Mask_Debug); mRootNode->addChild(newGroup); mGroups[actor] = newGroup; diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 53673d8a22..1dc42db47c 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -196,32 +197,6 @@ namespace return 0.0f; } - /// @brief Base class for visitors that remove nodes from a scene graph. - /// Subclasses need to fill the mToRemove vector. - /// To use, node->accept(removeVisitor); removeVisitor.remove(); - class RemoveVisitor : public osg::NodeVisitor - { - public: - RemoveVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - - void remove() - { - for (RemoveVec::iterator it = mToRemove.begin(); it != mToRemove.end(); ++it) - { - if (!it->second->removeChild(it->first)) - Log(Debug::Error) << "Error removing " << it->first->getName(); - } - } - - protected: - // - typedef std::vector > RemoveVec; - std::vector > mToRemove; - }; - class GetExtendedBonesVisitor : public osg::NodeVisitor { public: @@ -244,7 +219,7 @@ namespace std::vector > mFoundBones; }; - class RemoveFinishedCallbackVisitor : public RemoveVisitor + class RemoveFinishedCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -289,7 +264,7 @@ namespace } }; - class RemoveCallbackVisitor : public RemoveVisitor + class RemoveCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; @@ -397,90 +372,6 @@ namespace private: int mEffectId; }; - - // Removes all drawables from a graph. - class CleanObjectRootVisitor : public RemoveVisitor - { - public: - void apply(osg::Drawable& drw) override - { - applyDrawable(drw); - } - - void apply(osg::Group& node) override - { - applyNode(node); - } - void apply(osg::MatrixTransform& node) override - { - applyNode(node); - } - void apply(osg::Node& node) override - { - applyNode(node); - } - - void applyNode(osg::Node& node) - { - if (node.getStateSet()) - node.setStateSet(nullptr); - - if (node.getNodeMask() == 0x1 && node.getNumParents() == 1) - mToRemove.emplace_back(&node, node.getParent(0)); - else - traverse(node); - } - void applyDrawable(osg::Node& node) - { - osg::NodePath::iterator parent = getNodePath().end()-2; - // We know that the parent is a Group because only Groups can have children. - osg::Group* parentGroup = static_cast(*parent); - - // Try to prune nodes that would be empty after the removal - if (parent != getNodePath().begin()) - { - // This could be extended to remove the parent's parent, and so on if they are empty as well. - // But for NIF files, there won't be a benefit since only TriShapes can be set to STATIC dataVariance. - osg::Group* parentParent = static_cast(*(parent - 1)); - if (parentGroup->getNumChildren() == 1 && parentGroup->getDataVariance() == osg::Object::STATIC) - { - mToRemove.emplace_back(parentGroup, parentParent); - return; - } - } - - mToRemove.emplace_back(&node, parentGroup); - } - }; - - class RemoveTriBipVisitor : public RemoveVisitor - { - public: - void apply(osg::Drawable& drw) override - { - applyImpl(drw); - } - - void apply(osg::Group& node) override - { - traverse(node); - } - void apply(osg::MatrixTransform& node) override - { - traverse(node); - } - - void applyImpl(osg::Node& node) - { - const std::string toFind = "tri bip"; - if (Misc::StringUtils::ciCompareLen(node.getName(), toFind, toFind.size()) == 0) - { - osg::Group* parent = static_cast(*(getNodePath().end()-2)); - // Not safe to remove in apply(), since the visitor is still iterating the child list - mToRemove.emplace_back(&node, parent); - } - } - }; } namespace MWRender @@ -701,8 +592,6 @@ namespace MWRender void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) { - const std::map& index = mResourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -710,21 +599,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-3, 3, "/"); - mResourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) - addSingleAnimSource(name, baseModel); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "kf") + addSingleAnimSource(name, baseModel); } } @@ -1405,8 +1283,6 @@ namespace MWRender if (model.empty()) return; - const std::map& index = resourceSystem->getVFS()->getIndex(); - std::string animationPath = model; if (animationPath.find("meshes") == 0) { @@ -1414,21 +1290,10 @@ namespace MWRender } animationPath.replace(animationPath.size()-4, 4, "/"); - resourceSystem->getVFS()->normalizeFilename(animationPath); - - std::map::const_iterator found = index.lower_bound(animationPath); - while (found != index.end()) + for (const auto& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { - const std::string& name = found->first; - if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".nif") == 0) - loadBonesFromFile(node, name, resourceSystem); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "nif") + loadBonesFromFile(node, name, resourceSystem); } } @@ -1678,9 +1543,6 @@ namespace MWRender SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); - // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - node->accept(disableFreezeOnCullVisitor); node->setNodeMask(Mask_Effect); params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength(); @@ -1842,7 +1704,7 @@ namespace MWRender mRootController = addRotateController("bip01"); } - RotateController* Animation::addRotateController(std::string bone) + RotateController* Animation::addRotateController(const std::string &bone) { auto iter = getNodeMap().find(bone); if (iter == getNodeMap().end()) diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 24478439c1..ffcd9b3260 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -271,7 +271,7 @@ protected: float mLegsYawRadians; float mBodyPitchRadians; - RotateController* addRotateController(std::string bone); + RotateController* addRotateController(const std::string& bone); bool mHasMagicEffects; diff --git a/apps/openmw/mwrender/bulletdebugdraw.cpp b/apps/openmw/mwrender/bulletdebugdraw.cpp index 207b0ee504..9155132871 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.cpp +++ b/apps/openmw/mwrender/bulletdebugdraw.cpp @@ -4,16 +4,25 @@ #include #include +#include #include #include +#include #include +#include #include #include #include "bulletdebugdraw.hpp" #include "vismask.hpp" +#include +#include + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + namespace MWRender { @@ -26,44 +35,69 @@ DebugDrawer::DebugDrawer(osg::ref_ptr parentNode, btCollisionWorld * void DebugDrawer::createGeometry() { - if (!mGeometry) + if (!mLinesGeometry) { - mGeometry = new osg::Geometry; - mGeometry->setNodeMask(Mask_Debug); + mLinesGeometry = new osg::Geometry; + mTrisGeometry = new osg::Geometry; + mLinesGeometry->setNodeMask(Mask_Debug); + mTrisGeometry->setNodeMask(Mask_Debug); - mVertices = new osg::Vec3Array; - mColors = new osg::Vec4Array; + mLinesVertices = new osg::Vec3Array; + mTrisVertices = new osg::Vec3Array; + mLinesColors = new osg::Vec4Array; - mDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); + mLinesDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::LINES); + mTrisDrawArrays = new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES); - mGeometry->setUseDisplayList(false); - mGeometry->setVertexArray(mVertices); - mGeometry->setColorArray(mColors); - mGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); - mGeometry->setDataVariance(osg::Object::DYNAMIC); - mGeometry->addPrimitiveSet(mDrawArrays); + mLinesGeometry->setUseDisplayList(false); + mLinesGeometry->setVertexArray(mLinesVertices); + mLinesGeometry->setColorArray(mLinesColors); + mLinesGeometry->setColorBinding(osg::Geometry::BIND_PER_VERTEX); + mLinesGeometry->setDataVariance(osg::Object::DYNAMIC); + mLinesGeometry->addPrimitiveSet(mLinesDrawArrays); - mParentNode->addChild(mGeometry); + mTrisGeometry->setUseDisplayList(false); + mTrisGeometry->setVertexArray(mTrisVertices); + mTrisGeometry->setDataVariance(osg::Object::DYNAMIC); + mTrisGeometry->addPrimitiveSet(mTrisDrawArrays); + + mParentNode->addChild(mLinesGeometry); + mParentNode->addChild(mTrisGeometry); auto* stateSet = new osg::StateSet; stateSet->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), osg::StateAttribute::ON); + stateSet->setAttributeAndModes(new osg::PolygonOffset(SceneUtil::getReverseZ() ? 1.0 : -1.0, SceneUtil::getReverseZ() ? 1.0 : -1.0)); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateSet->setAttribute(material); + mLinesGeometry->setStateSet(stateSet); + mTrisGeometry->setStateSet(stateSet); mShapesRoot = new osg::Group; mShapesRoot->setStateSet(stateSet); mShapesRoot->setDataVariance(osg::Object::DYNAMIC); mShapesRoot->setNodeMask(Mask_Debug); mParentNode->addChild(mShapesRoot); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mLinesGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mTrisGeometry, "debug"); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mShapesRoot, "debug"); } } void DebugDrawer::destroyGeometry() { - if (mGeometry) + if (mLinesGeometry) { - mParentNode->removeChild(mGeometry); + mParentNode->removeChild(mLinesGeometry); + mParentNode->removeChild(mTrisGeometry); mParentNode->removeChild(mShapesRoot); - mGeometry = nullptr; - mVertices = nullptr; - mDrawArrays = nullptr; + mLinesGeometry = nullptr; + mLinesVertices = nullptr; + mLinesColors = nullptr; + mLinesDrawArrays = nullptr; + mTrisGeometry = nullptr; + mTrisVertices = nullptr; + mTrisDrawArrays = nullptr; } } @@ -76,24 +110,60 @@ void DebugDrawer::step() { if (mDebugOn) { - mVertices->clear(); - mColors->clear(); + mLinesVertices->clear(); + mTrisVertices->clear(); + mLinesColors->clear(); mShapesRoot->removeChildren(0, mShapesRoot->getNumChildren()); mWorld->debugDrawWorld(); showCollisions(); - mDrawArrays->setCount(mVertices->size()); - mVertices->dirty(); - mColors->dirty(); - mGeometry->dirtyBound(); + mLinesDrawArrays->setCount(mLinesVertices->size()); + mTrisDrawArrays->setCount(mTrisVertices->size()); + mLinesVertices->dirty(); + mTrisVertices->dirty(); + mLinesColors->dirty(); + mLinesGeometry->dirtyBound(); + mTrisGeometry->dirtyBound(); } } void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to, const btVector3 &color) { - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,1,1,1}); - mColors->push_back({1,1,1,1}); + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({1,1,1,1}); + mLinesColors->push_back({1,1,1,1}); + +#if BT_BULLET_VERSION < 317 + size_t size = mLinesVertices->size(); + if (size >= 6 + && (*mLinesVertices)[size - 1] == (*mLinesVertices)[size - 6] + && (*mLinesVertices)[size - 2] == (*mLinesVertices)[size - 3] + && (*mLinesVertices)[size - 4] == (*mLinesVertices)[size - 5]) + { + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mTrisVertices->push_back(mLinesVertices->back()); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + mLinesVertices->pop_back(); + mLinesColors->pop_back(); + } +#endif +} + +void DebugDrawer::drawTriangle(const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) +{ + mTrisVertices->push_back(Misc::Convert::toOsg(v0)); + mTrisVertices->push_back(Misc::Convert::toOsg(v1)); + mTrisVertices->push_back(Misc::Convert::toOsg(v2)); } void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) @@ -108,10 +178,10 @@ void DebugDrawer::showCollisions() { if (now - created < std::chrono::seconds(2)) { - mVertices->push_back(Misc::Convert::toOsg(from)); - mVertices->push_back(Misc::Convert::toOsg(to)); - mColors->push_back({1,0,0,1}); - mColors->push_back({1,0,0,1}); + mLinesVertices->push_back(Misc::Convert::toOsg(from)); + mLinesVertices->push_back(Misc::Convert::toOsg(to)); + mLinesColors->push_back({1,0,0,1}); + mLinesColors->push_back({1,0,0,1}); } } mCollisionViews.erase(std::remove_if(mCollisionViews.begin(), mCollisionViews.end(), diff --git a/apps/openmw/mwrender/bulletdebugdraw.hpp b/apps/openmw/mwrender/bulletdebugdraw.hpp index b24640427d..cea5794ba7 100644 --- a/apps/openmw/mwrender/bulletdebugdraw.hpp +++ b/apps/openmw/mwrender/bulletdebugdraw.hpp @@ -37,10 +37,13 @@ private: protected: osg::ref_ptr mParentNode; btCollisionWorld *mWorld; - osg::ref_ptr mGeometry; - osg::ref_ptr mVertices; - osg::ref_ptr mColors; - osg::ref_ptr mDrawArrays; + osg::ref_ptr mLinesGeometry; + osg::ref_ptr mTrisGeometry; + osg::ref_ptr mLinesVertices; + osg::ref_ptr mTrisVertices; + osg::ref_ptr mLinesColors; + osg::ref_ptr mLinesDrawArrays; + osg::ref_ptr mTrisDrawArrays; bool mDebugOn; @@ -56,6 +59,8 @@ public: void drawLine(const btVector3& from,const btVector3& to,const btVector3& color) override; + void drawTriangle(const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) override; + void addCollision(const btVector3& orig, const btVector3& normal); void showCollisions(); diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 3e5d1d0b31..edb017c2a2 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -251,6 +251,7 @@ namespace MWRender const float cameraObstacleLimit = 5.0f; const float focalObstacleLimit = 10.f; + const int collisionType = (MWPhysics::CollisionType::CollisionType_Default & ~MWPhysics::CollisionType::CollisionType_Actor); const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); @@ -260,7 +261,7 @@ namespace MWRender float offsetLen = focalOffset.length(); if (offsetLen > 0) { - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal - focalOffset, focal, focalObstacleLimit, collisionType); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; @@ -274,7 +275,7 @@ namespace MWRender mCameraDistance = std::min(mCameraDistance, mMaxNextCameraDistance); osg::Vec3d cameraPos; getPosition(focal, cameraPos); - MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit); + MWPhysics::RayCastingResult result = rayCasting->castSphere(focal, cameraPos, cameraObstacleLimit, collisionType); if (result.mHit) mCameraDistance = (result.mHitPos + result.mHitNormal * cameraObstacleLimit - focal).length(); } diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index e88c4cee32..44b7b2f457 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -131,6 +131,43 @@ namespace MWRender newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); newStateSet->addUniform(mNoAlphaUniform); } + 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); } @@ -187,6 +224,9 @@ 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); SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 2987111621..f1d28b0634 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -158,7 +158,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot) try { - osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(itemModel); + osg::ref_ptr node = mResourceSystem->getSceneManager()->getTemplate(itemModel); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); diff --git a/apps/openmw/mwrender/globalmap.cpp b/apps/openmw/mwrender/globalmap.cpp index 8784eb501a..7d7c6e45df 100644 --- a/apps/openmw/mwrender/globalmap.cpp +++ b/apps/openmw/mwrender/globalmap.cpp @@ -15,6 +15,7 @@ #include #include +#include #include @@ -24,6 +25,7 @@ #include "../mwworld/esmstore.hpp" #include "vismask.hpp" +#include "util.hpp" namespace { @@ -270,17 +272,9 @@ namespace MWRender void GlobalMap::worldPosToImageSpace(float x, float z, float& imageX, float& imageY) { - imageX = float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1); + imageX = (float(x / float(Constants::CellSizeInUnits) - mMinX) / (mMaxX - mMinX + 1)) * getWidth(); - imageY = 1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1); - } - - void GlobalMap::cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY) - { - imageX = float(x - mMinX) / (mMaxX - mMinX + 1); - - // NB y + 1, because we want the top left corner, not bottom left where the origin of the cell is - imageY = 1.f-float(y - mMinY + 1) / (mMaxY - mMinY + 1); + imageY = (1.f-float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1)) * getHeight(); } void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, @@ -331,7 +325,7 @@ namespace MWRender if (texture) { osg::ref_ptr geom = createTexturedQuad(srcLeft, srcTop, srcRight, srcBottom); - osg::ref_ptr depth = new osg::Depth; + auto depth = SceneUtil::createDepth(); depth->setWriteMask(0); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); diff --git a/apps/openmw/mwrender/globalmap.hpp b/apps/openmw/mwrender/globalmap.hpp index b359c852be..fd8a8d1016 100644 --- a/apps/openmw/mwrender/globalmap.hpp +++ b/apps/openmw/mwrender/globalmap.hpp @@ -41,12 +41,8 @@ namespace MWRender int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } - int getCellSize() const { return mCellSize; } - void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); - void cellTopLeftCornerToImageSpace(int x, int y, float& imageX, float& imageY); - void exploreCell (int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay diff --git a/apps/openmw/mwrender/groundcover.cpp b/apps/openmw/mwrender/groundcover.cpp index dd64c851f1..71bddb1f9d 100644 --- a/apps/openmw/mwrender/groundcover.cpp +++ b/apps/openmw/mwrender/groundcover.cpp @@ -1,11 +1,13 @@ #include "groundcover.hpp" #include +#include #include #include #include #include +#include #include "apps/openmw/mwworld/esmstore.hpp" #include "apps/openmw/mwbase/environment.hpp" @@ -26,36 +28,6 @@ namespace MWRender } } - void GroundcoverUpdater::setWindSpeed(float windSpeed) - { - mWindSpeed = windSpeed; - } - - void GroundcoverUpdater::setPlayerPos(osg::Vec3f playerPos) - { - mPlayerPos = playerPos; - } - - void GroundcoverUpdater::setDefaults(osg::StateSet *stateset) - { - osg::ref_ptr windUniform = new osg::Uniform("windSpeed", 0.0f); - stateset->addUniform(windUniform.get()); - - osg::ref_ptr playerPosUniform = new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f)); - stateset->addUniform(playerPosUniform.get()); - } - - void GroundcoverUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv) - { - osg::ref_ptr windUniform = stateset->getUniform("windSpeed"); - if (windUniform != nullptr) - windUniform->set(mWindSpeed); - - osg::ref_ptr playerPosUniform = stateset->getUniform("playerPos"); - if (playerPosUniform != nullptr) - playerPosUniform->set(mPlayerPos); - } - class InstancingVisitor : public osg::NodeVisitor { public: @@ -66,18 +38,6 @@ namespace MWRender { } - void apply(osg::Node& node) override - { - osg::ref_ptr ss = node.getStateSet(); - if (ss != nullptr) - { - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); - } - - traverse(node); - } - void apply(osg::Geometry& geom) override { for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) @@ -110,32 +70,14 @@ namespace MWRender // Display lists do not support instancing in OSG 3.4 geom.setUseDisplayList(false); + geom.setUseVertexBufferObjects(true); geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); - - osg::ref_ptr ss = geom.getOrCreateStateSet(); - ss->setAttribute(new osg::VertexAttribDivisor(6, 1)); - ss->setAttribute(new osg::VertexAttribDivisor(7, 1)); - - ss->removeAttribute(osg::StateAttribute::MATERIAL); - removeAlpha(ss); - - traverse(geom); } private: std::vector mInstances; osg::Vec3f mChunkPosition; - - void removeAlpha(osg::StateSet* stateset) - { - // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties - stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); - stateset->removeMode(GL_ALPHA_TEST); - stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); - stateset->removeMode(GL_BLEND); - stateset->setRenderBinToInherit(); - } }; class DensityCalculator @@ -172,7 +114,7 @@ namespace MWRender osg::Vec3f pos = ref.mPos.asVec3(); osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) - || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (maxBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) return false; return true; @@ -180,11 +122,11 @@ 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) { - ChunkId id = std::make_tuple(center, size, activeGrid); + GroundcoverChunkId id = std::make_tuple(center, size); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { InstanceMap instances; @@ -196,10 +138,23 @@ namespace MWRender } Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density) - : GenericResourceManager(nullptr) + : GenericResourceManager(nullptr) , mSceneManager(sceneManager) , mDensity(density) + , mStateset(new osg::StateSet) { + // MGE uses default alpha settings for groundcover, so we can not rely on alpha properties + // Force a unified alpha handling instead of data from meshes + osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); + mStateset->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + mStateset->setAttributeAndModes(new osg::BlendFunc, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + mStateset->setRenderBinDetails(0, "RenderBin", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); + mStateset->setAttribute(new osg::VertexAttribDivisor(6, 1)); + mStateset->setAttribute(new osg::VertexAttribDivisor(7, 1)); + + mProgramTemplate = mSceneManager->getShaderManager().getProgramTemplate() ? static_cast(mSceneManager->getShaderManager().getProgramTemplate()->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; + mProgramTemplate->addBindAttribLocation("aOffset", 6); + mProgramTemplate->addBindAttribLocation("aRotation", 7); } void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center) @@ -241,7 +196,7 @@ namespace MWRender if (model.empty()) continue; model = "meshes/" + model; - instances[model].emplace_back(std::move(ref), std::move(model)); + instances[model].emplace_back(std::move(ref)); } } } @@ -255,27 +210,23 @@ namespace MWRender for (auto& pair : instances) { const osg::Node* temp = mSceneManager->getTemplate(pair.first); - osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_ALL&(~osg::CopyOp::DEEP_COPY_TEXTURES))); + osg::ref_ptr node = static_cast(temp->clone(osg::CopyOp::DEEP_COPY_NODES|osg::CopyOp::DEEP_COPY_DRAWABLES|osg::CopyOp::DEEP_COPY_USERDATA|osg::CopyOp::DEEP_COPY_ARRAYS|osg::CopyOp::DEEP_COPY_PRIMITIVES)); // Keep link to original mesh to keep it in cache group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp)); - mSceneManager->reinstateRemovedState(node); - InstancingVisitor visitor(pair.second, worldCenter); node->accept(visitor); group->addChild(node); } - // Force a unified alpha handling instead of data from meshes - osg::ref_ptr alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f / 255.f); - group->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON); - group->getBound(); + group->setStateSet(mStateset); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) group->setCullCallback(new SceneUtil::LightListCallback); - mSceneManager->recreateShaders(group, "groundcover", false, true); - + mSceneManager->recreateShaders(group, "groundcover", false, true, mProgramTemplate); + mSceneManager->shareState(group); + group->getBound(); return group; } diff --git a/apps/openmw/mwrender/groundcover.hpp b/apps/openmw/mwrender/groundcover.hpp index b92ab97c95..ed88f7fe24 100644 --- a/apps/openmw/mwrender/groundcover.hpp +++ b/apps/openmw/mwrender/groundcover.hpp @@ -3,34 +3,13 @@ #include #include -#include #include +#include namespace MWRender { - class GroundcoverUpdater : public SceneUtil::StateSetUpdater - { - public: - GroundcoverUpdater() - : mWindSpeed(0.f) - , mPlayerPos(osg::Vec3f()) - { - } - - void setWindSpeed(float windSpeed); - void setPlayerPos(osg::Vec3f playerPos); - - protected: - void setDefaults(osg::StateSet *stateset) override; - void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override; - - private: - float mWindSpeed; - osg::Vec3f mPlayerPos; - }; - - typedef std::tuple ChunkId; // Center, Size, ActiveGrid - class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager + typedef std::tuple GroundcoverChunkId; // Center, Size + class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: Groundcover(Resource::SceneManager* sceneManager, float density); @@ -46,16 +25,16 @@ namespace MWRender { ESM::Position mPos; float mScale; - std::string mModel; - GroundcoverEntry(const ESM::CellRef& ref, const std::string& model): - mPos(ref.mPos), mScale(ref.mScale), mModel(model) + GroundcoverEntry(const ESM::CellRef& ref) : mPos(ref.mPos), mScale(ref.mScale) {} }; private: Resource::SceneManager* mSceneManager; float mDensity; + osg::ref_ptr mStateset; + osg::ref_ptr mProgramTemplate; 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 24c00048d9..02f59e0ed0 100644 --- a/apps/openmw/mwrender/localmap.cpp +++ b/apps/openmw/mwrender/localmap.cpp @@ -30,6 +30,7 @@ #include "../mwworld/cellstore.hpp" #include "vismask.hpp" +#include "util.hpp" namespace { @@ -118,14 +119,15 @@ const osg::Vec2f LocalMap::rotatePoint(const osg::Vec2f& point, const osg::Vec2f void LocalMap::clear() { - mSegments.clear(); + mExteriorSegments.clear(); + mInteriorSegments.clear(); } void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) { if (!mInterior) { - const MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + const MapSegment& segment = mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (segment.mFogOfWarImage && segment.mHasFogState) { @@ -155,7 +157,7 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) { for (int y = 0; y < segments.second; ++y) { - const MapSegment& segment = mSegments[std::make_pair(x,y)]; + const MapSegment& segment = mInteriorSegments[std::make_pair(x,y)]; fog->mFogTextures.emplace_back(); @@ -175,7 +177,12 @@ void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, float width, float height, const osg::Vec3d& upVector, float zmin, float zmax) { osg::ref_ptr camera (new osg::Camera); - camera->setProjectionMatrixAsOrtho(-width/2, width/2, -height/2, height/2, 5, (zmax-zmin) + 10); + + if (SceneUtil::getReverseZ()) + 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); + camera->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); camera->setViewMatrixAsLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF_INHERIT_VIEWPOINT); @@ -194,6 +201,13 @@ osg::ref_ptr LocalMap::createOrthographicCamera(float x, float y, f 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); + + stateset->addUniform(new osg::Uniform("projectionMatrix", static_cast(camera->getProjectionMatrix())), osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); + // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) osg::ref_ptr fog (new osg::Fog); @@ -249,26 +263,10 @@ void LocalMap::setupRenderToTexture(osg::ref_ptr camera, int x, int mRoot->addChild(camera); mActiveCameras.push_back(camera); - MapSegment& segment = mSegments[std::make_pair(x, y)]; + MapSegment& segment = mInterior? mInteriorSegments[std::make_pair(x, y)] : mExteriorSegments[std::make_pair(x, y)]; segment.mMapTexture = texture; } -bool needUpdate(std::set >& renderedGrid, std::set >& currentGrid, int cellX, int cellY) -{ - // if all the cells of the current grid are contained in the rendered grid then we can keep the old render - for (int dx=-1;dx<2;dx+=1) - { - for (int dy=-1;dy<2;dy+=1) - { - bool haveInRenderedGrid = renderedGrid.find(std::make_pair(cellX+dx,cellY+dy)) != renderedGrid.end(); - bool haveInCurrentGrid = currentGrid.find(std::make_pair(cellX+dx,cellY+dy)) != currentGrid.end(); - if (haveInCurrentGrid && !haveInRenderedGrid) - return true; - } - } - return false; -} - void LocalMap::requestMap(const MWWorld::CellStore* cell) { if (cell->isExterior()) @@ -276,13 +274,13 @@ void LocalMap::requestMap(const MWWorld::CellStore* cell) int cellX = cell->getCell()->getGridX(); int cellY = cell->getCell()->getGridY(); - MapSegment& segment = mSegments[std::make_pair(cellX, cellY)]; - if (!needUpdate(segment.mGrid, mCurrentGrid, cellX, cellY)) + MapSegment& segment = mExteriorSegments[std::make_pair(cellX, cellY)]; + if (!segment.needUpdate) return; else { - segment.mGrid = mCurrentGrid; requestExteriorMap(cell); + segment.needUpdate = false; } } else @@ -292,27 +290,27 @@ void LocalMap::requestMap(const MWWorld::CellStore* cell) void LocalMap::addCell(MWWorld::CellStore *cell) { if (cell->isExterior()) - mCurrentGrid.emplace(cell->getCell()->getGridX(), cell->getCell()->getGridY()); + mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())].needUpdate = true; +} + +void LocalMap::removeExteriorCell(int x, int y) +{ + mExteriorSegments.erase({ x, y }); } void LocalMap::removeCell(MWWorld::CellStore *cell) { saveFogOfWar(cell); - if (cell->isExterior()) - { - std::pair coords = std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()); - mSegments.erase(coords); - mCurrentGrid.erase(coords); - } - else - mSegments.clear(); + if (!cell->isExterior()) + mInteriorSegments.clear(); } osg::ref_ptr LocalMap::getMapTexture(int x, int y) { - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) return osg::ref_ptr(); else return found->second.mMapTexture; @@ -320,8 +318,9 @@ osg::ref_ptr LocalMap::getMapTexture(int x, int y) osg::ref_ptr LocalMap::getFogOfWarTexture(int x, int y) { - SegmentMap::iterator found = mSegments.find(std::make_pair(x, y)); - if (found == mSegments.end()) + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + SegmentMap::iterator found = segments.find(std::make_pair(x, y)); + if (found == segments.end()) return osg::ref_ptr(); else return found->second.mFogOfWarTexture; @@ -371,7 +370,7 @@ void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell) osg::Vec3d(0,1,0), zmin, zmax); setupRenderToTexture(camera, cell->getCell()->getGridX(), cell->getCell()->getGridY()); - MapSegment& segment = mSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; + MapSegment& segment = mExteriorSegments[std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY())]; if (!segment.mFogOfWarImage) { if (cell->getFog()) @@ -512,7 +511,7 @@ void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) setupRenderToTexture(camera, x, y); auto coords = std::make_pair(x,y); - MapSegment& segment = mSegments[coords]; + MapSegment& segment = mInteriorSegments[coords]; if (!segment.mFogOfWarImage) { bool loaded = false; @@ -558,7 +557,8 @@ osg::Vec2f LocalMap::interiorMapToWorldPosition (float nX, float nY, int x, int bool LocalMap::isPositionExplored (float nX, float nY, int x, int y) { - const MapSegment& segment = mSegments[std::make_pair(x, y)]; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + const MapSegment& segment = segments[std::make_pair(x, y)]; if (!segment.mFogOfWarImage) return false; @@ -630,7 +630,8 @@ void LocalMap::updatePlayer (const osg::Vec3f& position, const osg::Quat& orient int texX = x + mx; int texY = y + my*-1; - MapSegment& segment = mSegments[std::make_pair(texX, texY)]; + auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); + MapSegment& segment = segments[std::make_pair(texX, texY)]; if (!segment.mFogOfWarImage || !segment.mMapTexture) continue; diff --git a/apps/openmw/mwrender/localmap.hpp b/apps/openmw/mwrender/localmap.hpp index e586f8fb02..f9ccd5a011 100644 --- a/apps/openmw/mwrender/localmap.hpp +++ b/apps/openmw/mwrender/localmap.hpp @@ -50,6 +50,7 @@ namespace MWRender void requestMap (const MWWorld::CellStore* cell); void addCell(MWWorld::CellStore* cell); + void removeExteriorCell(int x, int y); void removeCell (MWWorld::CellStore* cell); @@ -126,13 +127,14 @@ namespace MWRender osg::ref_ptr mFogOfWarTexture; osg::ref_ptr mFogOfWarImage; - Grid mGrid; // the grid that was active at the time of rendering this segment + bool needUpdate = true; bool mHasFogState; }; typedef std::map, MapSegment> SegmentMap; - SegmentMap mSegments; + SegmentMap mExteriorSegments; + SegmentMap mInteriorSegments; int mMapResolution; diff --git a/apps/openmw/mwrender/navmesh.cpp b/apps/openmw/mwrender/navmesh.cpp index 791c41a1a0..523f7531af 100644 --- a/apps/openmw/mwrender/navmesh.cpp +++ b/apps/openmw/mwrender/navmesh.cpp @@ -2,9 +2,14 @@ #include "vismask.hpp" #include +#include +#include #include +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + namespace MWRender { NavMesh::NavMesh(const osg::ref_ptr& root, bool enabled) @@ -45,6 +50,7 @@ namespace MWRender mGroup = SceneUtil::createNavMeshGroup(navMesh, settings); if (mGroup) { + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mGroup, "debug"); mGroup->setNodeMask(Mask_Debug); mRootNode->addChild(mGroup); } diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index b538f0b7b6..bcb2d36f5d 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -43,6 +43,7 @@ #include "rotatecontroller.hpp" #include "renderbin.hpp" #include "vismask.hpp" +#include "util.hpp" namespace { @@ -286,37 +287,36 @@ NpcAnimation::NpcType NpcAnimation::getNpcType(const MWWorld::Ptr& ptr) return curType; } -static NpcAnimation::PartBoneMap createPartListMap() +static const inline NpcAnimation::PartBoneMap createPartListMap() { - NpcAnimation::PartBoneMap result; - result.insert(std::make_pair(ESM::PRT_Head, "Head")); - result.insert(std::make_pair(ESM::PRT_Hair, "Head")); // note it uses "Head" as attach bone, but "Hair" as filter - result.insert(std::make_pair(ESM::PRT_Neck, "Neck")); - result.insert(std::make_pair(ESM::PRT_Cuirass, "Chest")); - result.insert(std::make_pair(ESM::PRT_Groin, "Groin")); - result.insert(std::make_pair(ESM::PRT_Skirt, "Groin")); - result.insert(std::make_pair(ESM::PRT_RHand, "Right Hand")); - result.insert(std::make_pair(ESM::PRT_LHand, "Left Hand")); - result.insert(std::make_pair(ESM::PRT_RWrist, "Right Wrist")); - result.insert(std::make_pair(ESM::PRT_LWrist, "Left Wrist")); - result.insert(std::make_pair(ESM::PRT_Shield, "Shield Bone")); - result.insert(std::make_pair(ESM::PRT_RForearm, "Right Forearm")); - result.insert(std::make_pair(ESM::PRT_LForearm, "Left Forearm")); - result.insert(std::make_pair(ESM::PRT_RUpperarm, "Right Upper Arm")); - result.insert(std::make_pair(ESM::PRT_LUpperarm, "Left Upper Arm")); - result.insert(std::make_pair(ESM::PRT_RFoot, "Right Foot")); - result.insert(std::make_pair(ESM::PRT_LFoot, "Left Foot")); - result.insert(std::make_pair(ESM::PRT_RAnkle, "Right Ankle")); - result.insert(std::make_pair(ESM::PRT_LAnkle, "Left Ankle")); - result.insert(std::make_pair(ESM::PRT_RKnee, "Right Knee")); - result.insert(std::make_pair(ESM::PRT_LKnee, "Left Knee")); - result.insert(std::make_pair(ESM::PRT_RLeg, "Right Upper Leg")); - result.insert(std::make_pair(ESM::PRT_LLeg, "Left Upper Leg")); - result.insert(std::make_pair(ESM::PRT_RPauldron, "Right Clavicle")); - result.insert(std::make_pair(ESM::PRT_LPauldron, "Left Clavicle")); - result.insert(std::make_pair(ESM::PRT_Weapon, "Weapon Bone")); // Fallback. The real node name depends on the current weapon type. - result.insert(std::make_pair(ESM::PRT_Tail, "Tail")); - return result; + return { + {ESM::PRT_Head, "Head"}, + {ESM::PRT_Hair, "Head"}, // note it uses "Head" as attach bone, but "Hair" as filter + {ESM::PRT_Neck, "Neck"}, + {ESM::PRT_Cuirass, "Chest"}, + {ESM::PRT_Groin, "Groin"}, + {ESM::PRT_Skirt, "Groin"}, + {ESM::PRT_RHand, "Right Hand"}, + {ESM::PRT_LHand, "Left Hand"}, + {ESM::PRT_RWrist, "Right Wrist"}, + {ESM::PRT_LWrist, "Left Wrist"}, + {ESM::PRT_Shield, "Shield Bone"}, + {ESM::PRT_RForearm, "Right Forearm"}, + {ESM::PRT_LForearm, "Left Forearm"}, + {ESM::PRT_RUpperarm, "Right Upper Arm"}, + {ESM::PRT_LUpperarm, "Left Upper Arm"}, + {ESM::PRT_RFoot, "Right Foot"}, + {ESM::PRT_LFoot, "Left Foot"}, + {ESM::PRT_RAnkle, "Right Ankle"}, + {ESM::PRT_LAnkle, "Left Ankle"}, + {ESM::PRT_RKnee, "Right Knee"}, + {ESM::PRT_LKnee, "Left Knee"}, + {ESM::PRT_RLeg, "Right Upper Leg"}, + {ESM::PRT_LLeg, "Left Upper Leg"}, + {ESM::PRT_RPauldron, "Right Clavicle"}, + {ESM::PRT_LPauldron, "Left Clavicle"}, + {ESM::PRT_Weapon, "Weapon Bone"}, // Fallback. The real node name depends on the current weapon type. + {ESM::PRT_Tail, "Tail"}}; } const NpcAnimation::PartBoneMap NpcAnimation::sPartList = createPartListMap(); @@ -373,7 +373,7 @@ class DepthClearCallback : public osgUtil::RenderBin::DrawCallback public: DepthClearCallback() { - mDepth = new osg::Depth; + mDepth = SceneUtil::createDepth(); mDepth->setWriteMask(true); } @@ -551,7 +551,7 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } -std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +std::string NpcAnimation::getShieldMesh(const MWWorld::ConstPtr& shield) const { std::string mesh = shield.getClass().getModel(shield); const ESM::Armor *armor = shield.get()->mBase; @@ -714,14 +714,14 @@ void NpcAnimation::updateParts() PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const std::string& bonename, const std::string& bonefilter, bool enchantedGlow, osg::Vec4f* glowColor) { - osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model); + 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(instance, mObjectRoot, bonefilter, found->second); + osg::ref_ptr attached = SceneUtil::attach(templateNode, mObjectRoot, bonefilter, found->second); if (enchantedGlow) mGlowUpdater = SceneUtil::addEnchantedGlow(attached, mResourceSystem, *glowColor); @@ -1101,34 +1101,6 @@ Resource::ResourceSystem* NpcAnimation::getResourceSystem() return mResourceSystem; } -void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) -{ - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - if (isNew) - { - static const std::string schools[] = { - "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" - }; - - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(!magicEffect->mHitSound.empty()) - sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f); - else - sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f); - } - - if (!magicEffect->mHit.empty()) - { - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find (magicEffect->mHit); - bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; - // Don't play particle VFX unless the effect is new or it should be looping. - if (isNew || loop) - addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle); - } -} - void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 7e55001daf..9f7a186c5e 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -31,7 +31,6 @@ class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWor { public: void equipmentChanged() override; - void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override; public: typedef std::map PartBoneMap; @@ -106,7 +105,7 @@ private: protected: void addControllers() override; bool isArrowAttached() const override; - std::string getShieldMesh(MWWorld::ConstPtr shield) const override; + std::string getShieldMesh(const MWWorld::ConstPtr& shield) const override; public: /** diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 6b5f9a6e34..88c3d4ba02 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -77,7 +77,7 @@ namespace MWRender osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { osg::ref_ptr node = createChunk(size, center, activeGrid, viewPoint, compile); @@ -272,7 +272,7 @@ namespace MWRender : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCurrentStateSet(nullptr) , mCurrentDistance(0.f) - , mAnalyzeMask(analyzeMask) {} + { setTraversalMask(analyzeMask); } typedef std::unordered_map StateSetCounter; struct Result @@ -283,9 +283,6 @@ namespace MWRender void apply(osg::Node& node) override { - if (!(node.getNodeMask() & mAnalyzeMask)) - return; - if (node.getStateSet()) mCurrentStateSet = node.getStateSet(); @@ -308,9 +305,6 @@ namespace MWRender } void apply(osg::Geometry& geom) override { - if (!(geom.getNodeMask() & mAnalyzeMask)) - return; - if (osg::Array* array = geom.getVertexArray()) mResult.mNumVerts += array->getNumElements(); @@ -345,7 +339,6 @@ namespace MWRender osg::StateSet* mCurrentStateSet; StateSetCounter mGlobalStateSetCounter; float mCurrentDistance; - osg::Node::NodeMask mAnalyzeMask; }; class DebugVisitor : public osg::NodeVisitor @@ -364,6 +357,7 @@ namespace MWRender osg::ref_ptr stateset = node.getStateSet() ? osg::clone(node.getStateSet(), osg::CopyOp::SHALLOW_COPY) : new osg::StateSet; stateset->setAttribute(m); stateset->addUniform(new osg::Uniform("colorMode", 0)); + stateset->addUniform(new osg::Uniform("emissiveMult", 1.f)); node.setStateSet(stateset); } }; @@ -389,7 +383,7 @@ namespace MWRender , mRefTrackerLocked(false) { mActiveGrid = Settings::Manager::getBool("object paging active grid", "Terrain"); - mDebugBatches = Settings::Manager::getBool("object paging debug batches", "Terrain"); + mDebugBatches = Settings::Manager::getBool("debug chunks", "Terrain"); mMergeFactor = Settings::Manager::getFloat("object paging merge factor", "Terrain"); mMinSize = Settings::Manager::getFloat("object paging min size", "Terrain"); mMinSizeMergeFactor = Settings::Manager::getFloat("object paging min size merge factor", "Terrain"); @@ -426,13 +420,11 @@ namespace MWRender ESM::MovedCellRef cMRef; cMRef.mRefNum.mIndex = 0; bool deleted = false; - while(cell->getNextRef(esm[index], ref, deleted, /*ignoreMoves*/true, &cMRef)) + bool moved = false; + while(cell->getNextRef(esm[index], ref, deleted, cMRef, moved)) { - if (cMRef.mRefNum.mIndex) - { - cMRef.mRefNum.mIndex = 0; - continue; // ignore refs that are moved - } + if (moved) + continue; if (std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) != cell->mMovedRefs.end()) continue; Misc::StringUtils::lowerCaseInPlace(ref.mRefID); @@ -485,8 +477,7 @@ namespace MWRender constexpr auto copyMask = ~Mask_UpdateVisitor; AnalyzeVisitor analyzeVisitor(copyMask); - osg::Vec3f center3 = { center.x(), center.y(), 0.f }; - analyzeVisitor.mCurrentDistance = (viewPoint - center3).length2(); + analyzeVisitor.mCurrentDistance = (viewPoint - worldCenter).length2(); float minSize = mMinSize; if (mMinSizeMergeFactor) minSize *= mMinSizeMergeFactor; @@ -499,7 +490,7 @@ namespace MWRender { osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE; if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y()) - || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) + || (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (maxBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y())) continue; } @@ -512,8 +503,8 @@ namespace MWRender continue; } - if (ref.mRefID == "prisonmarker" || ref.mRefID == "divinemarker" || ref.mRefID == "templemarker" || ref.mRefID == "northmarker") - continue; // marker objects that have a hardcoded function in the game logic, should be hidden from the player + if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefID)) + continue; int type = store.findStatic(ref.mRefID); std::string model = getModel(type, ref.mRefID, store); @@ -660,7 +651,7 @@ namespace MWRender } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); unsigned int options = SceneUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS|SceneUtil::Optimizer::REMOVE_REDUNDANT_NODES|SceneUtil::Optimizer::MERGE_GEOMETRY; - mSceneManager->shareState(mergeGroup); + optimizer.optimize(mergeGroup, options); group->addChild(mergeGroup); diff --git a/apps/openmw/mwrender/pathgrid.cpp b/apps/openmw/mwrender/pathgrid.cpp index c20e81bb2d..42d9150811 100644 --- a/apps/openmw/mwrender/pathgrid.cpp +++ b/apps/openmw/mwrender/pathgrid.cpp @@ -8,7 +8,10 @@ #include #include +#include #include +#include +#include #include "../mwbase/world.hpp" // these includes can be removed once the static-hack is gone #include "../mwbase/environment.hpp" @@ -112,6 +115,8 @@ void Pathgrid::enableCellPathgrid(const MWWorld::CellStore *store) osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(geometry, "debug"); + cellPathGrid->addChild(geometry); mPathGridRoot->addChild(cellPathGrid); diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp new file mode 100644 index 0000000000..f846a05872 --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -0,0 +1,266 @@ +#include "postprocessor.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "vismask.hpp" +#include "renderingmanager.hpp" + +namespace +{ + osg::ref_ptr createFullScreenTri() + { + osg::ref_ptr geom = new osg::Geometry; + + osg::ref_ptr verts = new osg::Vec3Array; + verts->push_back(osg::Vec3f(-1, -1, 0)); + verts->push_back(osg::Vec3f(-1, 3, 0)); + verts->push_back(osg::Vec3f(3, -1, 0)); + + geom->setVertexArray(verts); + + geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3)); + + return geom; + } + + class CullCallback : public osg::NodeCallback + { + public: + CullCallback() + : mLastFrameNumber(0) + { + } + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + osgUtil::RenderStage* renderStage = cv->getCurrentRenderStage(); + + unsigned int frame = nv->getTraversalNumber(); + if (frame != mLastFrameNumber) + { + mLastFrameNumber = frame; + + MWRender::PostProcessor* postProcessor = dynamic_cast(cv->getCurrentCamera()->getUserData()); + + if (!postProcessor) + { + Log(Debug::Error) << "Failed retrieving user data for master camera: FBO setup failed"; + traverse(node, nv); + return; + } + + if (!postProcessor->getMsaaFbo()) + { + renderStage->setFrameBufferObject(postProcessor->getFbo()); + } + else + { + renderStage->setMultisampleResolveFramebufferObject(postProcessor->getFbo()); + renderStage->setFrameBufferObject(postProcessor->getMsaaFbo()); + } + } + + traverse(node, nv); + } + + private: + unsigned int mLastFrameNumber; + }; + + struct ResizedCallback : osg::GraphicsContext::ResizedCallback + { + ResizedCallback(MWRender::PostProcessor* postProcessor) + : mPostProcessor(postProcessor) + { + } + + void resizedImplementation(osg::GraphicsContext* gc, int x, int y, int width, int height) override + { + gc->resizedImplementation(x, y, width, height); + mPostProcessor->resize(width, height); + } + + MWRender::PostProcessor* mPostProcessor; + }; +} + +namespace MWRender +{ + PostProcessor::PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode) + : mViewer(viewer) + , mRootNode(new osg::Group) + , mDepthFormat(GL_DEPTH_COMPONENT24) + , mRendering(rendering) + { + if (!SceneUtil::getReverseZ()) + return; + + osg::GraphicsContext* gc = viewer->getCamera()->getGraphicsContext(); + unsigned int contextID = gc->getState()->getContextID(); + osg::GLExtensions* ext = gc->getState()->get(); + + constexpr char errPreamble[] = "Postprocessing and floating point depth buffers disabled: "; + + if (!ext->isFrameBufferObjectSupported) + { + Log(Debug::Warning) << errPreamble << "FrameBufferObject unsupported."; + return; + } + + if (Settings::Manager::getInt("antialiasing", "Video") > 1 && !ext->isRenderbufferMultisampleSupported()) + { + Log(Debug::Warning) << errPreamble << "RenderBufferMultiSample unsupported. Disabling antialiasing will resolve this issue."; + 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."; + return; + } + + int width = viewer->getCamera()->getViewport()->width(); + int height = viewer->getCamera()->getViewport()->height(); + + createTexturesAndCamera(width, height); + resize(width, height); + + mRootNode->addChild(mHUDCamera); + mRootNode->addChild(rootNode); + mViewer->setSceneData(mRootNode); + + // We need to manually set the FBO and resolve FBO during the cull callback. If we were using a separate + // RTT camera this would not be needed. + mViewer->getCamera()->addCullCallback(new CullCallback); + mViewer->getCamera()->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + mViewer->getCamera()->attach(osg::Camera::COLOR_BUFFER0, mSceneTex); + mViewer->getCamera()->attach(osg::Camera::DEPTH_BUFFER, mDepthTex); + + mViewer->getCamera()->getGraphicsContext()->setResizedCallback(new ResizedCallback(this)); + mViewer->getCamera()->setUserData(this); + } + + void PostProcessor::resize(int width, int height) + { + mDepthTex->setTextureSize(width, height); + mSceneTex->setTextureSize(width, height); + mDepthTex->dirtyTextureObject(); + mSceneTex->dirtyTextureObject(); + + int samples = Settings::Manager::getInt("antialiasing", "Video"); + + mFbo = new osg::FrameBufferObject; + mFbo->setAttachment(osg::Camera::COLOR_BUFFER0, osg::FrameBufferAttachment(mSceneTex)); + mFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(mDepthTex)); + + // When MSAA is enabled we must first render to a render buffer, then + // blit the result to the FBO which is either passed to the main frame + // buffer for display or used as the entry point for a post process chain. + if (samples > 1) + { + mMsaaFbo = new osg::FrameBufferObject; + osg::ref_ptr colorRB = new osg::RenderBuffer(width, height, mSceneTex->getInternalFormat(), samples); + osg::ref_ptr depthRB = new osg::RenderBuffer(width, height, mDepthTex->getInternalFormat(), samples); + mMsaaFbo->setAttachment(osg::Camera::COLOR_BUFFER0, osg::FrameBufferAttachment(colorRB)); + mMsaaFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(depthRB)); + } + + mViewer->getCamera()->resize(width, height); + mHUDCamera->resize(width, height); + mRendering.updateProjectionMatrix(); + } + + void PostProcessor::createTexturesAndCamera(int width, int height) + { + mDepthTex = new osg::Texture2D; + mDepthTex->setTextureSize(width, height); + mDepthTex->setSourceFormat(GL_DEPTH_COMPONENT); + mDepthTex->setSourceType(SceneUtil::isFloatingPointDepthFormat(getDepthFormat()) ? GL_FLOAT : GL_UNSIGNED_INT); + mDepthTex->setInternalFormat(mDepthFormat); + mDepthTex->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST); + mDepthTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::NEAREST); + mDepthTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mDepthTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mDepthTex->setResizeNonPowerOfTwoHint(false); + + mSceneTex = new osg::Texture2D; + mSceneTex->setTextureSize(width, height); + mSceneTex->setSourceFormat(GL_RGB); + mSceneTex->setSourceType(GL_UNSIGNED_BYTE); + mSceneTex->setInternalFormat(GL_RGB); + mSceneTex->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::NEAREST); + mSceneTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::NEAREST); + mSceneTex->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mSceneTex->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mSceneTex->setResizeNonPowerOfTwoHint(false); + + mHUDCamera = new osg::Camera; + mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER); + mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0)); + mHUDCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1, 0, 1)); + mHUDCamera->setAllowEventFocus(false); + mHUDCamera->setViewport(0, 0, width, height); + + // Shaders calculate correct UV coordinates for our fullscreen triangle + constexpr char vertSrc[] = R"GLSL( + #version 120 + + varying vec2 uv; + + void main() + { + gl_Position = vec4(gl_Vertex.xy, 0.0, 1.0); + uv = gl_Position.xy * 0.5 + 0.5; + } + )GLSL"; + + constexpr char fragSrc[] = R"GLSL( + #version 120 + + varying vec2 uv; + uniform sampler2D sceneTex; + + void main() + { + gl_FragData[0] = texture2D(sceneTex, uv); + } + )GLSL"; + + osg::ref_ptr vertShader = new osg::Shader(osg::Shader::VERTEX, vertSrc); + osg::ref_ptr fragShader = new osg::Shader(osg::Shader::FRAGMENT, fragSrc); + + osg::ref_ptr program = new osg::Program; + program->addShader(vertShader); + program->addShader(fragShader); + + mHUDCamera->addChild(createFullScreenTri()); + mHUDCamera->setNodeMask(Mask_RenderToTexture); + + auto* stateset = mHUDCamera->getOrCreateStateSet(); + stateset->setTextureAttributeAndModes(0, mSceneTex, osg::StateAttribute::ON); + stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + stateset->addUniform(new osg::Uniform("sceneTex", 0)); + stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + } + +} diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp new file mode 100644 index 0000000000..f93f3a5b66 --- /dev/null +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H +#define OPENMW_MWRENDER_POSTPROCESSOR_H + +#include +#include +#include +#include +#include + +namespace osgViewer +{ + class Viewer; +} + +namespace MWRender +{ + class RenderingManager; + + class PostProcessor : public osg::Referenced + { + public: + PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode); + + auto getMsaaFbo() { return mMsaaFbo; } + auto getFbo() { return mFbo; } + + int getDepthFormat() { return mDepthFormat; } + + void resize(int width, int height); + + private: + void createTexturesAndCamera(int width, int height); + + osgViewer::Viewer* mViewer; + osg::ref_ptr mRootNode; + osg::ref_ptr mHUDCamera; + + osg::ref_ptr mMsaaFbo; + osg::ref_ptr mFbo; + + osg::ref_ptr mSceneTex; + osg::ref_ptr mDepthTex; + + int mDepthFormat; + + RenderingManager& mRendering; + }; +} + +#endif \ No newline at end of file diff --git a/apps/openmw/mwrender/recastmesh.cpp b/apps/openmw/mwrender/recastmesh.cpp index 7c7c6b8eb1..5afa78cd93 100644 --- a/apps/openmw/mwrender/recastmesh.cpp +++ b/apps/openmw/mwrender/recastmesh.cpp @@ -1,11 +1,15 @@ #include "recastmesh.hpp" #include +#include +#include #include #include "vismask.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" namespace MWRender { RecastMesh::RecastMesh(const osg::ref_ptr& root, bool enabled) @@ -49,6 +53,7 @@ namespace MWRender || it->second.mRevision != tile->second->getRevision()) { const auto group = SceneUtil::createRecastMeshGroup(*tile->second, settings); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); group->setNodeMask(Mask_Debug); mRootNode->removeChild(it->second.mValue); mRootNode->addChild(group); @@ -66,6 +71,7 @@ namespace MWRender if (mGroups.count(tile.first)) continue; const auto group = SceneUtil::createRecastMeshGroup(*tile.second, settings); + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(group, "debug"); group->setNodeMask(Mask_Debug); mGroups.emplace(tile.first, Group {tile.second->getGeneration(), tile.second->getRevision(), group}); mRootNode->addChild(group); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index e497fdecd2..be143510b2 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include @@ -68,9 +70,105 @@ #include "objectpaging.hpp" #include "screenshotmanager.hpp" #include "groundcover.hpp" +#include "postprocessor.hpp" namespace MWRender { + class SharedUniformStateUpdater : public SceneUtil::StateSetUpdater + { + public: + SharedUniformStateUpdater(bool usePlayerUniforms) + : mLinearFac(0.f) + , mNear(0.f) + , mFar(0.f) + , mUsePlayerUniforms(usePlayerUniforms) + , mWindSpeed(0.f) + { + } + + void setDefaults(osg::StateSet *stateset) override + { + stateset->addUniform(new osg::Uniform("projectionMatrix", osg::Matrixf{})); + stateset->addUniform(new osg::Uniform("linearFac", 0.f)); + stateset->addUniform(new osg::Uniform("near", 0.f)); + stateset->addUniform(new osg::Uniform("far", 0.f)); + if (mUsePlayerUniforms) + { + stateset->addUniform(new osg::Uniform("windSpeed", 0.0f)); + stateset->addUniform(new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f))); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + auto* uProjectionMatrix = stateset->getUniform("projectionMatrix"); + if (uProjectionMatrix) + uProjectionMatrix->set(mProjectionMatrix); + + auto* uLinearFac = stateset->getUniform("linearFac"); + if (uLinearFac) + uLinearFac->set(mLinearFac); + + auto* uNear = stateset->getUniform("near"); + if (uNear) + uNear->set(mNear); + + auto* uFar = stateset->getUniform("far"); + if (uFar) + uFar->set(mFar); + + if (mUsePlayerUniforms) + { + auto* windSpeed = stateset->getUniform("windSpeed"); + if (windSpeed) + windSpeed->set(mWindSpeed); + + auto* playerPos = stateset->getUniform("playerPos"); + if (playerPos) + playerPos->set(mPlayerPos); + } + } + + void setProjectionMatrix(const osg::Matrixf& projectionMatrix) + { + mProjectionMatrix = projectionMatrix; + } + + void setLinearFac(float linearFac) + { + mLinearFac = linearFac; + } + + void setNear(float near) + { + mNear = near; + } + + void setFar(float far) + { + mFar = far; + } + + void setWindSpeed(float windSpeed) + { + mWindSpeed = windSpeed; + } + + void setPlayerPos(osg::Vec3f playerPos) + { + mPlayerPos = playerPos; + } + + + private: + osg::Matrixf mProjectionMatrix; + float mLinearFac; + float mNear; + float mFar; + bool mUsePlayerUniforms; + float mWindSpeed; + osg::Vec3f mPlayerPos; + }; class StateUpdater : public SceneUtil::StateSetUpdater { @@ -164,7 +262,7 @@ namespace MWRender try { for (std::vector::const_iterator it = mModels.begin(); it != mModels.end(); ++it) - mResourceSystem->getSceneManager()->cacheInstance(*it); + mResourceSystem->getSceneManager()->getTemplate(*it); for (std::vector::const_iterator it = mTextures.begin(); it != mTextures.end(); ++it) mResourceSystem->getImageManager()->getImage(*it); for (std::vector::const_iterator it = mKeyframes.begin(); it != mKeyframes.end(); ++it) @@ -198,15 +296,22 @@ namespace MWRender , mFieldOfViewOverridden(false) , mFieldOfViewOverride(0.f) { + bool reverseZ = SceneUtil::getReverseZ(); + + if (reverseZ) + Log(Debug::Info) << "Using reverse-z depth buffer"; + else + Log(Debug::Info) << "Using standard depth buffer"; + auto lightingMethod = SceneUtil::LightManager::getLightingMethodFromString(Settings::Manager::getString("lighting method", "Shaders")); resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); - resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); // Shadows and radial fog have problems with fixed-function mode bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows") - || lightingMethod != SceneUtil::LightingMethod::FFP; + || lightingMethod != SceneUtil::LightingMethod::FFP + || reverseZ; resourceSystem->getSceneManager()->setForceShaders(forceShaders); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); @@ -220,7 +325,6 @@ namespace MWRender // Let LightManager choose which backend to use based on our hint. For methods besides legacy lighting, this depends on support for various OpenGL extensions. osg::ref_ptr sceneRoot = new SceneUtil::LightManager(lightingMethod == SceneUtil::LightingMethod::FFP); - resourceSystem->getSceneManager()->getShaderManager().setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setLightingMethod(sceneRoot->getLightingMethod()); resourceSystem->getSceneManager()->setSupportedLightingMethods(sceneRoot->getSupportedLightingMethods()); mMinimumAmbientLuminance = std::clamp(Settings::Manager::getFloat("minimum interior brightness", "Shaders"), 0.f, 1.f); @@ -268,6 +372,8 @@ namespace MWRender globalDefines["groundcoverStompMode"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp mode", "Groundcover"), 0, 2)); globalDefines["groundcoverStompIntensity"] = std::to_string(std::clamp(Settings::Manager::getInt("stomp intensity", "Groundcover"), 0, 2)); + globalDefines["reverseZ"] = reverseZ ? "1" : "0"; + // It is unnecessary to stop/start the viewer as no frames are being rendered yet. mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines); @@ -297,7 +403,9 @@ namespace MWRender mTerrainStorage.reset(new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps)); const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain"); - if (Settings::Manager::getBool("distant terrain", "Terrain")) + bool groundcover = Settings::Manager::getBool("enabled", "Groundcover"); + bool distantTerrain = Settings::Manager::getBool("distant terrain", "Terrain"); + if (distantTerrain || groundcover) { const int compMapResolution = Settings::Manager::getInt("composite map resolution", "Terrain"); int compMapPower = Settings::Manager::getInt("composite map level", "Terrain"); @@ -306,9 +414,10 @@ namespace MWRender const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain"); float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain"); maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f); + bool debugChunks = Settings::Manager::getBool("debug chunks", "Terrain"); mTerrain.reset(new Terrain::QuadTreeWorld( sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, - compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize)); + compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks)); if (Settings::Manager::getBool("object paging", "Terrain")) { mObjectPaging.reset(new ObjectPaging(mResourceSystem->getSceneManager())); @@ -322,38 +431,30 @@ namespace MWRender mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells")); mTerrain->setWorkQueue(mWorkQueue.get()); - if (Settings::Manager::getBool("enabled", "Groundcover")) + if (groundcover) { - osg::ref_ptr groundcoverRoot = new osg::Group; - groundcoverRoot->setNodeMask(Mask_Groundcover); - groundcoverRoot->setName("Groundcover Root"); - sceneRoot->addChild(groundcoverRoot); - - mGroundcoverUpdater = new GroundcoverUpdater; - groundcoverRoot->addUpdateCallback(mGroundcoverUpdater); - - float chunkSize = Settings::Manager::getFloat("min chunk size", "Groundcover"); - if (chunkSize >= 1.0f) - chunkSize = 1.0f; - else if (chunkSize >= 0.5f) - chunkSize = 0.5f; - else if (chunkSize >= 0.25f) - chunkSize = 0.25f; - else if (chunkSize != 0.125f) - chunkSize = 0.125f; - float density = Settings::Manager::getFloat("density", "Groundcover"); density = std::clamp(density, 0.f, 1.f); - mGroundcoverWorld.reset(new Terrain::QuadTreeWorld(groundcoverRoot, mTerrainStorage.get(), Mask_Groundcover, lodFactor, chunkSize)); mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density)); - static_cast(mGroundcoverWorld.get())->addChunkManager(mGroundcover.get()); + static_cast(mTerrain.get())->addChunkManager(mGroundcover.get()); mResourceSystem->addResourceManager(mGroundcover.get()); - // Groundcover it is handled in the same way indifferently from if it is from active grid or from distant cell. - // Use a stub grid to avoid splitting between chunks for active grid and chunks for distant cells. - mGroundcoverWorld->setActiveGrid(osg::Vec4i(0, 0, 0, 0)); + mGroundcover->setViewDistance(groundcoverDistance); } + + mStateUpdater = new StateUpdater; + sceneRoot->addUpdateCallback(mStateUpdater); + + mSharedUniformStateUpdater = new SharedUniformStateUpdater(groundcover); + rootNode->addUpdateCallback(mSharedUniformStateUpdater); + + mPostProcessor = new PostProcessor(*this, viewer, mRootNode); + resourceSystem->getSceneManager()->setDepthFormat(mPostProcessor->getDepthFormat()); + + 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."; + // water goes after terrain for correct waterculling order mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); @@ -385,6 +486,7 @@ namespace MWRender defaultMat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1)); defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); sceneRoot->getOrCreateStateSet()->setAttribute(defaultMat); + sceneRoot->getOrCreateStateSet()->addUniform(new osg::Uniform("emissiveMult", 1.f)); mFog.reset(new FogManager()); @@ -393,9 +495,6 @@ namespace MWRender source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); - mStateUpdater = new StateUpdater; - sceneRoot->addUpdateCallback(mStateUpdater); - osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING; if (!Settings::Manager::getBool("small feature culling", "Camera")) @@ -416,7 +515,9 @@ namespace MWRender NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); - mNearClip = Settings::Manager::getFloat("near clip", "Camera"); + // 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); @@ -424,8 +525,6 @@ namespace MWRender mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("near", mNearClip)); - mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); // Hopefully, anything genuinely requiring the default alpha func of GL_ALWAYS explicitly sets it @@ -433,8 +532,15 @@ namespace MWRender // The transparent renderbin sets alpha testing on because that was faster on old GPUs. It's now slower and breaks things. mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF); - mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); - mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); + 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(clipcontrol, osg::StateAttribute::ON); + } + + SceneUtil::setCameraClearDepth(mViewer->getCamera()); + updateProjectionMatrix(); } @@ -492,7 +598,7 @@ namespace MWRender workItem->mTextures.emplace_back("textures/_land_default.dds"); - mWorkQueue->addWorkItem(workItem); + mWorkQueue->addWorkItem(std::move(workItem)); } double RenderingManager::getReferenceTime() const @@ -600,8 +706,6 @@ namespace MWRender if (store->getCell()->isExterior()) { mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); - if (mGroundcoverWorld) - mGroundcoverWorld->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } } void RenderingManager::removeCell(const MWWorld::CellStore *store) @@ -613,8 +717,6 @@ namespace MWRender if (store->getCell()->isExterior()) { mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); - if (mGroundcoverWorld) - mGroundcoverWorld->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } mWater->removeCell(store); @@ -625,8 +727,6 @@ namespace MWRender if (!enable) mWater->setCullCallback(nullptr); mTerrain->enable(enable); - if (mGroundcoverWorld) - mGroundcoverWorld->enable(enable); } void RenderingManager::setSkyEnabled(bool enabled) @@ -716,15 +816,12 @@ namespace MWRender mSky->update(dt); mWater->update(dt); - if (mGroundcoverUpdater) - { - const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); + osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - float windSpeed = mSky->getBaseWindSpeed(); - mGroundcoverUpdater->setWindSpeed(windSpeed); - mGroundcoverUpdater->setPlayerPos(playerPos); - } + float windSpeed = mSky->getBaseWindSpeed(); + mSharedUniformStateUpdater->setWindSpeed(windSpeed); + mSharedUniformStateUpdater->setPlayerPos(playerPos); } updateNavMesh(); @@ -1067,22 +1164,25 @@ namespace MWRender float fov = mFieldOfView; if (mFieldOfViewOverridden) fov = mFieldOfViewOverride; + mViewer->getCamera()->setProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance); - mUniformNear->set(mNearClip); - mUniformFar->set(mViewDistance); + if (SceneUtil::getReverseZ()) + { + mSharedUniformStateUpdater->setLinearFac(-mNearClip / (mViewDistance - mNearClip) - 1.f); + mSharedUniformStateUpdater->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspective(fov, aspect, mNearClip, mViewDistance)); + } + else + mSharedUniformStateUpdater->setProjectionMatrix(mViewer->getCamera()->getProjectionMatrix()); + + mSharedUniformStateUpdater->setNear(mNearClip); + mSharedUniformStateUpdater->setFar(mViewDistance); // Since our fog is not radial yet, we should take FOV in account, otherwise terrain near viewing distance may disappear. // Limit FOV here just for sure, otherwise viewing distance can be too high. fov = std::min(mFieldOfView, 140.f); float distanceMult = std::cos(osg::DegreesToRadians(fov)/2.f); mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f/distanceMult : 1.f)); - - if (mGroundcoverWorld) - { - float groundcoverDistance = std::max(0.f, Settings::Manager::getFloat("rendering distance", "Groundcover")); - mGroundcoverWorld->setViewDistance(groundcoverDistance * (distanceMult ? 1.f/distanceMult : 1.f)); - } } void RenderingManager::updateTextureFiltering() diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a0a74bd5c4..bd8bbe4694 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -70,8 +70,8 @@ namespace DetourNavigator namespace MWRender { - class GroundcoverUpdater; class StateUpdater; + class SharedUniformStateUpdater; class EffectManager; class ScreenshotManager; @@ -89,6 +89,7 @@ namespace MWRender class RecastMesh; class ObjectPaging; class Groundcover; + class PostProcessor; class RenderingManager : public MWRender::RenderingInterface { @@ -108,9 +109,6 @@ namespace MWRender SceneUtil::UnrefQueue* getUnrefQueue(); Terrain::World* getTerrain(); - osg::Uniform* mUniformNear; - osg::Uniform* mUniformFar; - void preloadCommonAssets(); double getReferenceTime() const; @@ -240,8 +238,9 @@ namespace MWRender bool pagingUnlockCache(); void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); - private: void updateProjectionMatrix(); + + private: void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); @@ -262,8 +261,6 @@ namespace MWRender osg::ref_ptr mSceneRoot; Resource::ResourceSystem* mResourceSystem; - osg::ref_ptr mGroundcoverUpdater; - osg::ref_ptr mWorkQueue; osg::ref_ptr mUnrefQueue; @@ -278,7 +275,6 @@ namespace MWRender std::unique_ptr mObjects; std::unique_ptr mWater; std::unique_ptr mTerrain; - std::unique_ptr mGroundcoverWorld; std::unique_ptr mTerrainStorage; std::unique_ptr mObjectPaging; std::unique_ptr mGroundcover; @@ -287,6 +283,7 @@ namespace MWRender std::unique_ptr mScreenshotManager; std::unique_ptr mEffectManager; std::unique_ptr mShadowManager; + osg::ref_ptr mPostProcessor; osg::ref_ptr mPlayerAnimation; osg::ref_ptr mPlayerNode; std::unique_ptr mCamera; @@ -294,6 +291,7 @@ namespace MWRender osg::Vec3f mCurrentCameraPos; osg::ref_ptr mStateUpdater; + osg::ref_ptr mSharedUniformStateUpdater; osg::Vec4f mAmbientColor; float mMinimumAmbientLuminance; diff --git a/apps/openmw/mwrender/ripplesimulation.cpp b/apps/openmw/mwrender/ripplesimulation.cpp index 6788f53f44..fdfc3db63d 100644 --- a/apps/openmw/mwrender/ripplesimulation.cpp +++ b/apps/openmw/mwrender/ripplesimulation.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "vismask.hpp" @@ -55,13 +56,13 @@ namespace stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setTextureAttributeAndModes(0, textures[0], osg::StateAttribute::ON); - osg::ref_ptr depth (new osg::Depth); + auto depth = SceneUtil::createDepth(); depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); osg::ref_ptr polygonOffset (new osg::PolygonOffset); - polygonOffset->setUnits(-1); - polygonOffset->setFactor(-1); + polygonOffset->setUnits(SceneUtil::getReverseZ() ? 1 : -1); + polygonOffset->setFactor(SceneUtil::getReverseZ() ? 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 f474a5a9f6..5a047a1566 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -22,6 +23,7 @@ #include "util.hpp" #include "vismask.hpp" #include "water.hpp" +#include "postprocessor.hpp" namespace MWRender { @@ -88,6 +90,18 @@ namespace MWRender int topPadding = std::max(0, static_cast(screenH - screenW / imageaspect) / 2); int width = screenW - leftPadding*2; int height = screenH - topPadding*2; + + // Ensure we are reading from the resolved framebuffer and not the multisampled render buffer when in use. + // glReadPixel() cannot read from multisampled targets. + PostProcessor* postProcessor = dynamic_cast(renderInfo.getCurrentCamera()->getUserData()); + + if (postProcessor && postProcessor->getFbo() && postProcessor->getMsaaFbo()) + { + osg::GLExtensions* ext = osg::GLExtensions::Get(renderInfo.getContextID(), false); + if (ext) + ext->glBindFramebuffer(GL_FRAMEBUFFER_EXT, postProcessor->getFbo()->getHandle(renderInfo.getContextID())); + } + mImage->readPixels(leftPadding, topPadding, width, height, GL_RGB, GL_UNSIGNED_BYTE); mImage->scaleImage(mWidth, mHeight, 1); } @@ -177,6 +191,7 @@ namespace MWRender screenshotH = screenshotW; // use square resolution for planet mapping std::vector> images; + images.reserve(6); for (int i = 0; i < 6; ++i) images.push_back(new osg::Image); @@ -236,6 +251,7 @@ 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; @@ -282,9 +298,10 @@ namespace MWRender camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT,osg::Camera::PIXEL_BUFFER_RTT); - camera->setViewport(0, 0, w, h); + SceneUtil::setCameraClearDepth(camera); + osg::ref_ptr texture (new osg::Texture2D); texture->setInternalFormat(GL_RGB); texture->setTextureSize(w,h); @@ -312,20 +329,23 @@ namespace MWRender mRootNode->removeChild(camera); } - void ScreenshotManager::makeCubemapScreenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) + void ScreenshotManager::makeCubemapScreenshot(osg::Image *image, int w, int h, const osg::Matrixd& cameraTransform) { osg::ref_ptr rttCamera (new osg::Camera); float nearClip = Settings::Manager::getFloat("near clip", "Camera"); float viewDistance = Settings::Manager::getFloat("viewing distance", "Camera"); // each cubemap side sees 90 degrees - rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance); + if (SceneUtil::getReverseZ()) + rttCamera->setProjectionMatrix(SceneUtil::getReversedZProjectionMatrixAsPerspectiveInf(90.0, w/float(h), nearClip)); + else + rttCamera->setProjectionMatrixAsPerspective(90.0, w/float(h), nearClip, viewDistance); rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); rttCamera->setUpdateCallback(new NoTraverseCallback); rttCamera->addChild(mSceneRoot); - rttCamera->addChild(mWater->getReflectionCamera()); - rttCamera->addChild(mWater->getRefractionCamera()); + rttCamera->addChild(mWater->getReflectionNode()); + rttCamera->addChild(mWater->getRefractionNode()); rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); diff --git a/apps/openmw/mwrender/screenshotmanager.hpp b/apps/openmw/mwrender/screenshotmanager.hpp index 373fe3be84..094a4a20f4 100644 --- a/apps/openmw/mwrender/screenshotmanager.hpp +++ b/apps/openmw/mwrender/screenshotmanager.hpp @@ -37,7 +37,7 @@ namespace MWRender void traversalsAndWait(unsigned int frame); void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); - void makeCubemapScreenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); + void makeCubemapScreenshot(osg::Image* image, int w, int h, const osg::Matrixd &cameraTransform=osg::Matrixd()); }; } diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 8bac90604b..c1b79843a7 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -54,6 +54,7 @@ #include "vismask.hpp" #include "renderbin.hpp" +#include "util.hpp" namespace { @@ -594,13 +595,13 @@ private: osg::StateSet* queryStateSet = new osg::StateSet; if (queryVisible) { - osg::ref_ptr depth (new osg::Depth); - depth->setFunction(osg::Depth::LEQUAL); + 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. - depth->setZNear(1.0); - depth->setZFar(1.0); + double far = SceneUtil::getReverseZ() ? 0.0 : 1.0; + depth->setZNear(far); + depth->setZFar(far); depth->setWriteMask(false); queryStateSet->setAttributeAndModes(depth, osg::StateAttribute::ON); } @@ -1209,7 +1210,7 @@ void SkyManager::create() mCloudMesh2->addUpdateCallback(mCloudUpdater2); mCloudMesh2->setNodeMask(0); - osg::ref_ptr depth = new osg::Depth; + auto depth = SceneUtil::createDepth(); depth->setWriteMask(false); mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); @@ -1349,11 +1350,9 @@ void SkyManager::setCamera(osg::Camera *camera) class WrapAroundOperator : public osgParticle::Operator { public: - WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator() + WrapAroundOperator(osg::Camera *camera, const osg::Vec3 &wrapRange): osgParticle::Operator(), + mCamera(camera), mWrapRange(wrapRange), mHalfWrapRange(mWrapRange / 2.0) { - mCamera = camera; - mWrapRange = wrapRange; - mHalfWrapRange = mWrapRange / 2.0; mPreviousCameraPosition = getCameraPosition(); } @@ -1730,9 +1729,6 @@ void SkyManager::setWeather(const WeatherResult& weather) mParticleEffect->accept(alphaFaderSetupVisitor); - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - mParticleEffect->accept(disableFreezeOnCullVisitor); - SceneUtil::FindByClassVisitor findPSVisitor(std::string("ParticleSystem")); mParticleEffect->accept(findPSVisitor); diff --git a/apps/openmw/mwrender/terrainstorage.cpp b/apps/openmw/mwrender/terrainstorage.cpp index 528ce70ea3..879a2ef68b 100644 --- a/apps/openmw/mwrender/terrainstorage.cpp +++ b/apps/openmw/mwrender/terrainstorage.cpp @@ -33,7 +33,10 @@ namespace MWRender void TerrainStorage::getBounds(float& minX, float& maxX, float& minY, float& maxY) { - minX = 0, minY = 0, maxX = 0, maxY = 0; + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index c5fd1a3637..88e422fcf3 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -261,65 +262,44 @@ osg::ref_ptr readPngImage (const std::string& file) return result.getImage(); } - -class Refraction : public osg::Camera +class Refraction : public SceneUtil::RTTNode { public: - Refraction() + Refraction(uint32_t rttSize) + : RTTNode(rttSize, rttSize, 1, false) { - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setRenderOrder(osg::Camera::PRE_RENDER, 1); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("RefractionCamera"); - setCullCallback(new InheritViewPointCallback); - setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + mClipCullNode = new ClipCullNode; + } - 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); - setNodeMask(Mask_RenderToTexture); - setViewport(0, 0, rttSize, rttSize); + 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); - // No need for Update traversal since the scene is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); + 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) - osg::ref_ptr fog (new osg::Fog); + osg::ref_ptr fog(new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); - getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + camera->getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); - - mRefractionTexture = new osg::Texture2D; - mRefractionTexture->setTextureSize(rttSize, rttSize); - mRefractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setInternalFormat(GL_RGB); - mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mRefractionTexture); - - mRefractionDepthTexture = new osg::Texture2D; - mRefractionDepthTexture->setTextureSize(rttSize, rttSize); - mRefractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); - mRefractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setSourceType(GL_UNSIGNED_INT); - mRefractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + } + + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); } void setScene(osg::Node* scene) @@ -332,74 +312,54 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f,std::max(0.0f, + const float refractionScale = std::min(1.0f, std::max(0.0f, Settings::Manager::getFloat("refraction scale", "Water"))); - setViewMatrix(osg::Matrix::scale(1,1,refractionScale) * - osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel)); + mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * + osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); - } - - osg::Texture2D* getRefractionTexture() const - { - return mRefractionTexture.get(); - } - - osg::Texture2D* getRefractionDepthTexture() const - { - return mRefractionDepthTexture.get(); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } private: osg::ref_ptr mClipCullNode; - osg::ref_ptr mRefractionTexture; - osg::ref_ptr mRefractionDepthTexture; osg::ref_ptr mScene; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; }; -class Reflection : public osg::Camera +class Reflection : public SceneUtil::RTTNode { public: - Reflection(bool isInterior) + Reflection(uint32_t rttSize, bool isInterior) + : RTTNode(rttSize, rttSize, 0, false) { - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("ReflectionCamera"); - setCullCallback(new InheritViewPointCallback); - setInterior(isInterior); - setNodeMask(Mask_RenderToTexture); + mClipCullNode = new ClipCullNode; + } - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new NoTraverseCallback); - - mReflectionTexture = new osg::Texture2D; - mReflectionTexture->setTextureSize(rttSize, rttSize); - mReflectionTexture->setInternalFormat(GL_RGB); - mReflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mReflectionTexture); + 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"); + camera->addCullCallback(new InheritViewPointCallback); // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. - osg::ref_ptr frontFace (new osg::FrontFace); + osg::ref_ptr frontFace(new osg::FrontFace); frontFace->setMode(osg::FrontFace::CLOCKWISE); - getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); + camera->getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + } + + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setInterior(bool isInterior) @@ -409,16 +369,16 @@ public: 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 >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask); + mNodeMask = Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; } void setWaterLevel(float waterLevel) { - setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); + mViewMatrix = osg::Matrix::scale(1, 1, -1) * osg::Matrix::translate(0, 0, 2 * waterLevel); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, 1), osg::Vec3d(0, 0, waterLevel))); } void setScene(osg::Node* scene) @@ -429,15 +389,11 @@ public: mClipCullNode->addChild(scene); } - osg::Texture2D* getReflectionTexture() const - { - return mReflectionTexture.get(); - } - private: - osg::ref_ptr mReflectionTexture; osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; + osg::Node::NodeMask mNodeMask; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -474,6 +430,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mTop(0) , mInterior(false) , mCullCallback(nullptr) + , mShaderWaterStateSetUpdater(nullptr) { mSimulation.reset(new RippleSimulation(mSceneRoot, resourceSystem)); @@ -528,22 +485,31 @@ void Water::setCullCallback(osg::Callback* callback) void Water::updateWaterMaterial() { + if (mShaderWaterStateSetUpdater) + { + mWaterNode->removeCullCallback(mShaderWaterStateSetUpdater); + mShaderWaterStateSetUpdater = nullptr; + } if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } + mWaterNode->setStateSet(nullptr); + mWaterGeom->setStateSet(nullptr); + mWaterGeom->setUpdateCallback(nullptr); + if (Settings::Manager::getBool("shader", "Water")) { - mReflection = new Reflection(mInterior); + unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); + + mReflection = new Reflection(rttSize, mInterior); mReflection->setWaterLevel(mTop); mReflection->setScene(mSceneRoot); if (mCullCallback) @@ -552,7 +518,7 @@ void Water::updateWaterMaterial() if (Settings::Manager::getBool("refraction", "Water")) { - mRefraction = new Refraction; + mRefraction = new Refraction(rttSize); mRefraction->setWaterLevel(mTop); mRefraction->setScene(mSceneRoot); if (mCullCallback) @@ -560,7 +526,7 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } - createShaderWaterStateSet(mWaterGeom, mReflection, mRefraction); + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); @@ -568,16 +534,21 @@ void Water::updateWaterMaterial() updateVisible(); } -osg::Camera *Water::getReflectionCamera() +osg::Node *Water::getReflectionNode() { return mReflection; } -osg::Camera *Water::getRefractionCamera() +osg::Node* Water::getRefractionNode() { return mRefraction; } +osg::Vec3d Water::getPosition() const +{ + return mWaterNode->getPosition(); +} + void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); @@ -620,17 +591,76 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) sceneManager->setForceShaders(oldValue); } +class ShaderWaterStateSetUpdater : public SceneUtil::StateSetUpdater +{ +public: + ShaderWaterStateSetUpdater(Water* water, Reflection* reflection, Refraction* refraction, osg::ref_ptr program, osg::ref_ptr normalMap) + : mWater(water) + , mReflection(reflection) + , mRefraction(refraction) + , mProgram(program) + , mNormalMap(normalMap) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("normalMap", 0)); + stateset->setTextureAttributeAndModes(0, mNormalMap, osg::StateAttribute::ON); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("reflectionMap", 1)); + if (mRefraction) + { + stateset->addUniform(new osg::Uniform("refractionMap", 2)); + stateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); + stateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); + } + else + { + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + osg::ref_ptr depth = SceneUtil::createDepth(); + depth->setWriteMask(false); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + stateset->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWater->getPosition()))); + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + stateset->setTextureAttributeAndModes(1, mReflection->getColorTexture(cv), osg::StateAttribute::ON); + + if (mRefraction) + { + stateset->setTextureAttributeAndModes(2, mRefraction->getColorTexture(cv), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(3, mRefraction->getDepthTexture(cv), osg::StateAttribute::ON); + } + stateset->getUniform("nodePosition")->set(osg::Vec3f(mWater->getPosition())); + } + +private: + Water* mWater; + Reflection* mReflection; + Refraction* mRefraction; + osg::ref_ptr mProgram; + osg::ref_ptr mNormalMap; +}; + void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction) { // use a define map to conditionally compile the shader std::map defineMap; - defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); + defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(mRefraction ? "1" : "0"))); Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr vertexShader (shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr fragmentShader (shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader(shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr fragmentShader(shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr program = shaderMgr.getProgram(vertexShader, fragmentShader); - osg::ref_ptr normalMap (new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); + osg::ref_ptr normalMap(new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); if (normalMap->getImage()) normalMap->getImage()->flipVertical(); @@ -640,46 +670,11 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - osg::ref_ptr shaderStateset = new osg::StateSet; - shaderStateset->addUniform(new osg::Uniform("normalMap", 0)); - shaderStateset->addUniform(new osg::Uniform("reflectionMap", 1)); - - shaderStateset->setTextureAttributeAndModes(0, normalMap, osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(1, reflection->getReflectionTexture(), osg::StateAttribute::ON); - - if (refraction) - { - shaderStateset->setTextureAttributeAndModes(2, refraction->getRefractionTexture(), osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(3, refraction->getRefractionDepthTexture(), osg::StateAttribute::ON); - shaderStateset->addUniform(new osg::Uniform("refractionMap", 2)); - shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); - } - else - { - shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); - - osg::ref_ptr depth (new osg::Depth); - depth->setWriteMask(false); - shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - - shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - - osg::ref_ptr program (new osg::Program); - program->addShader(vertexShader); - program->addShader(fragmentShader); - auto method = mResourceSystem->getSceneManager()->getLightingMethod(); - if (method == SceneUtil::LightingMethod::SingleUBO) - program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); - shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); - - node->setStateSet(shaderStateset); - mRainIntensityUpdater = new RainIntensityUpdater(); node->setUpdateCallback(mRainIntensityUpdater); + + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, program, normalMap); + node->addCullCallback(mShaderWaterStateSetUpdater); } void Water::processChangedSettings(const Settings::CategorySettingVector& settings) @@ -693,13 +688,11 @@ Water::~Water() if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } @@ -739,11 +732,6 @@ void Water::changeCell(const MWWorld::CellStore* store) } if(mInterior != wasInterior && mReflection) mReflection->setInterior(mInterior); - - // create a new StateSet to prevent threading issues - osg::ref_ptr nodeStateSet (new osg::StateSet); - nodeStateSet->addUniform(new osg::Uniform("nodePosition", osg::Vec3f(mWaterNode->getPosition()))); - mWaterNode->setStateSet(nodeStateSet); } void Water::setHeight(const float height) diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index ec7dc95db8..870b8949c7 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -72,6 +72,7 @@ namespace MWRender bool mInterior; osg::Callback* mCullCallback; + osg::ref_ptr mShaderWaterStateSetUpdater; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); @@ -116,8 +117,10 @@ namespace MWRender void update(float dt); - osg::Camera *getReflectionCamera(); - osg::Camera *getRefractionCamera(); + osg::Node* getReflectionNode(); + osg::Node* getRefractionNode(); + + osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); }; diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 0c2a11466b..fb92a4980a 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -60,7 +60,7 @@ WeaponAnimation::~WeaponAnimation() } -void WeaponAnimation::attachArrow(MWWorld::Ptr actor) +void WeaponAnimation::attachArrow(const MWWorld::Ptr& actor) { const MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); MWWorld::ConstContainerStoreIterator weaponSlot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index d02107333e..333dccc915 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -34,7 +34,7 @@ namespace MWRender virtual ~WeaponAnimation(); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. - void attachArrow(MWWorld::Ptr actor); + void attachArrow(const MWWorld::Ptr &actor); void detachArrow(MWWorld::Ptr actor); diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index d4de8ded5d..c5a4bb6dfc 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -362,7 +362,15 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - const auto value = static_cast(ptr.getClass().getCreatureStats (ptr).getAiSequence().getLastRunTypeId()); + Interpreter::Type_Integer value = -1; + if(ptr.getClass().isActor()) + { + const auto& stats = ptr.getClass().getCreatureStats(ptr); + if(!stats.isDead() || !stats.isDeathAnimationFinished()) + { + value = static_cast(stats.getAiSequence().getLastRunTypeId()); + } + } runtime.push (value); } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 0b36d8bb48..3642f68dc2 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -25,6 +25,7 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" @@ -562,13 +563,7 @@ namespace MWScript const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects(); - effects += stats.getActiveSpells().getMagicEffects(); - if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished()) - { - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); - effects += store.getMagicEffects(); - } + const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); for (const auto& activeEffect : effects) { @@ -820,7 +815,7 @@ namespace MWScript } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id)); + runtime.push(stats.getActiveSpells().isSpellActive(id)); } }; @@ -863,6 +858,9 @@ namespace MWScript float param = runtime[0].mFloat; runtime.pop(); + if (param < 0) + throw std::runtime_error("square root of negative number (we aren't that imaginary)"); + runtime.push(std::sqrt (param)); } }; @@ -1233,8 +1231,11 @@ namespace MWScript if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(targetId, spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(targetId, spellId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1276,8 +1277,11 @@ namespace MWScript if (ptr.getClass().isActor()) { - MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); - ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) + { + MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + } return; } @@ -1356,18 +1360,19 @@ namespace MWScript std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); msg << std::put_time(std::gmtime(¤tTime), "%Y.%m.%d %T UTC") << std::endl; - msg << "Content file: "; + msg << "Content file: " << ptr.getCellRef().getRefNum().mContentFile; if (!ptr.getCellRef().hasContentFile()) - msg << "[None]" << std::endl; + msg << " [None]" << std::endl; else { std::vector contentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - msg << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << std::endl; - msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + msg << " [" << contentFiles.at (ptr.getCellRef().getRefNum().mContentFile) << "]" << std::endl; } + msg << "RefNum: " << ptr.getCellRef().getRefNum().mIndex << std::endl; + if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; if (!ptr.getRefData().getCount()) diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 073312f538..ccad186a0d 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -473,6 +473,8 @@ namespace MWScript ESM::Spell::SpellType type = static_cast(spell->mData.mType); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power) { + // Add spell effect to *this actor's* queue immediately + creatureStats.getActiveSpells().addSpell(spell, ptr); // Apply looping particles immediately for constant effects MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); } @@ -492,17 +494,6 @@ namespace MWScript runtime.pop(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); - // The spell may have an instant effect which must be handled before the spell's removal. - for (const auto& effect : creatureStats.getSpells().getMagicEffects()) - { - if (effect.second.getMagnitude() <= 0) - continue; - MWMechanics::CastSpell cast(ptr, ptr); - if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude())) - creatureStats.getSpells().purgeEffect(effect.first.mId); - } - - MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id); creatureStats.getSpells().remove (id); MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager(); @@ -527,8 +518,7 @@ namespace MWScript std::string spellid = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid); - ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(ptr, spellid); } }; @@ -544,7 +534,7 @@ namespace MWScript Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); - ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId); + ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; @@ -1205,6 +1195,7 @@ 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); // HACK: disable/enable object to re-add it to the scene properly (need a new Animation). MWBase::Environment::get().getWorld()->disable(ptr); diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index 096e78d34b..3d00b24ef8 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -167,17 +167,17 @@ namespace MWScript // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. // UWV axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(angle,ay,az),MWBase::RotationFlag_inverseOrder); else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,angle,az),MWBase::RotationFlag_inverseOrder); else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_inverseOrder); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,ay,angle),MWBase::RotationFlag_inverseOrder); else if (axis == "u") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(angle,ay,az),MWBase::RotationFlag_none); else if (axis == "w") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,angle,az),MWBase::RotationFlag_none); else if (axis == "v") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_none); + MWBase::Environment::get().getWorld()->rotateObject(ptr,osg::Vec3f(ax,ay,angle),MWBase::RotationFlag_none); } }; @@ -393,17 +393,17 @@ namespace MWScript if(store) { MWWorld::Ptr base = ptr; - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,x,y,z); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,store,osg::Vec3f(x,y,z)); dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; + auto rot = ptr.getRefData().getPosition().asRotationVec3(); // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) // except for when you position the player, then degrees must be used. // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. if(ptr != MWMechanics::getPlayer()) zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); + rot.z() = osg::DegreesToRadians(zRot); + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); ptr.getClass().adjustPosition(ptr, false); } @@ -444,22 +444,22 @@ namespace MWScript if (ptr == MWMechanics::getPlayer()) { MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr,cell,x,y,z); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, cell, osg::Vec3(x, y, z)); } else { - ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, x, y, z, true, true); + ptr = MWBase::Environment::get().getWorld()->moveObject(ptr, osg::Vec3f(x, y, z), true, true); } dynamic_cast(runtime.getContext()).updatePtr(base,ptr); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; + auto rot = ptr.getRefData().getPosition().asRotationVec3(); // Note that you must specify ZRot in minutes (1 degree = 60 minutes; north = 0, east = 5400, south = 10800, west = 16200) // except for when you position the player, then degrees must be used. // See "Morrowind Scripting for Dummies (9th Edition)" pages 50 and 54 for reference. if(ptr != MWMechanics::getPlayer()) zRot = zRot/60.0f; - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,osg::DegreesToRadians(zRot)); + rot.z() = osg::DegreesToRadians(zRot); + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); ptr.getClass().adjustPosition(ptr, false); } }; @@ -621,16 +621,14 @@ namespace MWScript Interpreter::Type_Float rotation = osg::DegreesToRadians(runtime[0].mFloat*MWBase::Environment::get().getFrameDuration()); runtime.pop(); - float ax = ptr.getRefData().getPosition().rot[0]; - float ay = ptr.getRefData().getPosition().rot[1]; - float az = ptr.getRefData().getPosition().rot[2]; - + auto rot = ptr.getRefData().getPosition().asRotationVec3(); if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax+rotation,ay,az); + rot.x() += rotation; else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay+rotation,az); + rot.y() += rotation; else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,az+rotation); + rot.z() += rotation; + MWBase::Environment::get().getWorld()->rotateObject(ptr,rot); } }; @@ -682,15 +680,10 @@ namespace MWScript if (!ptr.isInCell()) return; - float xr = ptr.getCellRef().getPosition().rot[0]; - float yr = ptr.getCellRef().getPosition().rot[1]; - float zr = ptr.getCellRef().getPosition().rot[2]; - - MWBase::Environment::get().getWorld()->rotateObject(ptr, xr, yr, zr); + MWBase::Environment::get().getWorld()->rotateObject(ptr, ptr.getCellRef().getPosition().asRotationVec3()); dynamic_cast(runtime.getContext()).updatePtr(ptr, - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().pos[0], - ptr.getCellRef().getPosition().pos[1], ptr.getCellRef().getPosition().pos[2])); + MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3())); } }; diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index cb71cb56d2..e64e89d775 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -131,7 +131,7 @@ namespace MWSound max = std::max(min, max); Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); - mVfs->normalizeFilename(sfx.mResourceName); + sfx.mResourceName = mVfs->normalizeFilename(sfx.mResourceName); mBufferNameMap.emplace(soundId, &sfx); return &sfx; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index fc9735d576..d2422870d7 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -294,20 +294,9 @@ namespace MWSound if (mMusicFiles.find(playlist) == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); - std::string pattern = "Music/" + playlist; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) - { - if (found->first.size() >= pattern.size() && found->first.substr(0, pattern.size()) == pattern) - filelist.push_back(found->first); - else - break; - ++found; - } + for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist)) + filelist.push_back(name); mMusicFiles[playlist] = filelist; } @@ -327,13 +316,11 @@ namespace MWSound if (mMusicFiles.find("Title") == mMusicFiles.end()) { std::vector filelist; - const std::map& index = mVFS->getIndex(); // Is there an ini setting for this filename or something? std::string filename = "music/special/morrowind title.mp3"; - auto found = index.find(filename); - if (found != index.end()) + if (mVFS->exists(filename)) { - filelist.emplace_back(found->first); + filelist.emplace_back(filename); mMusicFiles["Title"] = filelist; } else @@ -355,10 +342,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; @@ -389,10 +373,7 @@ namespace MWSound if(!mOutput->isInitialized()) return; - std::string voicefile = "Sound/"+filename; - - mVFS->normalizeFilename(voicefile); - DecoderPtr decoder = loadVoice(voicefile); + DecoderPtr decoder = loadVoice(mVFS->normalizeFilename("Sound/" + filename)); if (!decoder) return; diff --git a/apps/openmw/mwworld/actionteleport.cpp b/apps/openmw/mwworld/actionteleport.cpp index 9cd8469a39..56274eb9a0 100644 --- a/apps/openmw/mwworld/actionteleport.cpp +++ b/apps/openmw/mwworld/actionteleport.cpp @@ -55,10 +55,10 @@ namespace MWWorld int cellY; world->positionToIndex(mPosition.pos[0],mPosition.pos[1],cellX,cellY); world->moveObject(actor,world->getExterior(cellX,cellY), - mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + mPosition.asVec3(), true, true); } else - world->moveObject(actor,world->getInterior(mCellName),mPosition.pos[0],mPosition.pos[1],mPosition.pos[2]); + world->moveObject(actor,world->getInterior(mCellName),mPosition.asVec3(), true, true); } } diff --git a/apps/openmw/mwworld/cellpreloader.cpp b/apps/openmw/mwworld/cellpreloader.cpp index 44afde22ae..590e1e9c00 100644 --- a/apps/openmw/mwworld/cellpreloader.cpp +++ b/apps/openmw/mwworld/cellpreloader.cpp @@ -14,12 +14,36 @@ #include #include #include +#include #include "../mwrender/landmanager.hpp" #include "cellstore.hpp" #include "class.hpp" +namespace +{ + template + bool contains(const std::vector& container, + const Contained& contained, float tolerance) + { + for (const auto& pos : contained) + { + bool found = false; + for (const auto& pos2 : container) + { + if ((pos.first-pos2.first).length2() < tolerance*tolerance && pos.second == pos2.second) + { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } +} + namespace MWWorld { @@ -94,7 +118,6 @@ namespace MWWorld { mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, mSceneManager->getVFS()); - bool animated = false; size_t slashpos = mesh.find_last_of("/\\"); if (slashpos != std::string::npos && slashpos != mesh.size()-1) { @@ -106,17 +129,11 @@ namespace MWWorld { kfname.replace(kfname.size()-4, 4, ".kf"); if (mSceneManager->getVFS()->exists(kfname)) - { mPreloadedObjects.insert(mKeyframeManager->get(kfname)); - animated = true; - } } } } - if (mPreloadInstances && animated) - mPreloadedObjects.insert(mSceneManager->cacheInstance(mesh)); - else - mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); + mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else @@ -157,29 +174,20 @@ namespace MWWorld public: TerrainPreloadItem(const std::vector >& views, Terrain::World* world, const std::vector& preloadPositions) : mAbort(false) - , mProgress(views.size()) - , mProgressRange(0) , mTerrainViews(views) , mWorld(world) , mPreloadPositions(preloadPositions) { } - bool storeViews(double referenceTime) - { - for (unsigned int i=0; istoreView(mTerrainViews[i], referenceTime)) - return false; - return true; - } - void doWork() override { for (unsigned int i=0; ireset(); - mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mProgress[i], mProgressRange); + mWorld->preload(mTerrainViews[i], mPreloadPositions[i].first, mPreloadPositions[i].second, mAbort, mLoadingReporter); } + mLoadingReporter.complete(); } void abort() override @@ -187,16 +195,17 @@ namespace MWWorld mAbort = true; } - int getProgress() const { return !mProgress.empty() ? mProgress[0].load() : 0; } - int getProgressRange() const { return !mProgress.empty() && mProgress[0].load() ? mProgressRange : 0; } + void wait(Loading::Listener& listener) const + { + mLoadingReporter.wait(listener); + } private: std::atomic mAbort; - std::vector> mProgress; - int mProgressRange; std::vector > mTerrainViews; Terrain::World* mWorld; std::vector mPreloadPositions; + Loading::Reporter mLoadingReporter; }; /// Worker thread item: update the resource system's cache, effectively deleting unused entries. @@ -229,7 +238,7 @@ namespace MWWorld , mMaxCacheSize(0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) - , mStoreViewsFailCount(0) + , mLoadedTerrainTimestamp(0.0) { } @@ -317,7 +326,7 @@ namespace MWWorld if (found->second.mWorkItem) { found->second.mWorkItem->abort(); - mUnrefQueue->push(mPreloadCells[cell].mWorkItem); + mUnrefQueue->push(std::move(found->second.mWorkItem)); } mPreloadCells.erase(found); @@ -365,18 +374,8 @@ namespace MWWorld if (mTerrainPreloadItem && mTerrainPreloadItem->isDone()) { - if (!mTerrainPreloadItem->storeViews(timestamp)) - { - if (++mStoreViewsFailCount > 100) - { - OSG_ALWAYS << "paging views are rebuilt every frame, please check for faulty enable/disable scripts." << std::endl; - mStoreViewsFailCount = 0; - } - setTerrainPreloadPositions(std::vector()); - } - else - mStoreViewsFailCount = 0; - mTerrainPreloadItem = nullptr; + mLoadedTerrainPositions = mTerrainPreloadPositions; + mLoadedTerrainTimestamp = timestamp; } } @@ -415,38 +414,25 @@ namespace MWWorld mUnrefQueue = unrefQueue; } - bool CellPreloader::syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp) + bool CellPreloader::syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener) { if (!mTerrainPreloadItem) return true; else if (mTerrainPreloadItem->isDone()) { - if (mTerrainPreloadItem->storeViews(timestamp)) - { - mTerrainPreloadItem = nullptr; - return true; - } - else - { - setTerrainPreloadPositions(std::vector()); - setTerrainPreloadPositions(positions); - return false; - } + return true; } else { - progress = mTerrainPreloadItem->getProgress(); - progressRange = mTerrainPreloadItem->getProgressRange(); - return false; + mTerrainPreloadItem->wait(listener); + return true; } } void CellPreloader::abortTerrainPreloadExcept(const CellPreloader::PositionCellGrid *exceptPos) { - const float resetThreshold = ESM::Land::REAL_SIZE; - for (const auto& pos : mTerrainPreloadPositions) - if (exceptPos && (pos.first-exceptPos->first).length2() < resetThreshold*resetThreshold && pos.second == exceptPos->second) - return; + if (exceptPos && contains(mTerrainPreloadPositions, std::array {*exceptPos}, ESM::Land::REAL_SIZE)) + return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); @@ -455,29 +441,14 @@ namespace MWWorld setTerrainPreloadPositions(std::vector()); } - bool contains(const std::vector& container, const std::vector& contained) - { - for (const auto& pos : contained) - { - bool found = false; - for (const auto& pos2 : container) - { - if ((pos.first-pos2.first).length2() < 1 && pos.second == pos2.second) - { - found = true; - break; - } - } - if (!found) return false; - } - return true; - } - void CellPreloader::setTerrainPreloadPositions(const std::vector &positions) { if (positions.empty()) + { mTerrainPreloadPositions.clear(); - else if (contains(mTerrainPreloadPositions, positions)) + mLoadedTerrainPositions.clear(); + } + else if (contains(mTerrainPreloadPositions, positions, 128.f)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; @@ -503,5 +474,10 @@ namespace MWWorld } } } + + bool CellPreloader::isTerrainLoaded(const CellPreloader::PositionCellGrid &position, double referenceTime) const + { + return mLoadedTerrainTimestamp + mResourceSystem->getSceneManager()->getExpiryDelay() > referenceTime && contains(mLoadedTerrainPositions, std::array {position}, ESM::Land::REAL_SIZE); + } } diff --git a/apps/openmw/mwworld/cellpreloader.hpp b/apps/openmw/mwworld/cellpreloader.hpp index e719f2e606..0c3bcb4f99 100644 --- a/apps/openmw/mwworld/cellpreloader.hpp +++ b/apps/openmw/mwworld/cellpreloader.hpp @@ -29,6 +29,11 @@ namespace MWRender class LandManager; } +namespace Loading +{ + class Listener; +} + namespace MWWorld { class CellStore; @@ -72,8 +77,9 @@ namespace MWWorld typedef std::pair PositionCellGrid; void setTerrainPreloadPositions(const std::vector& positions); - bool syncTerrainLoad(const std::vector &positions, int& progress, int& progressRange, double timestamp); + bool syncTerrainLoad(const std::vector &positions, double timestamp, Loading::Listener& listener); void abortTerrainPreloadExcept(const PositionCellGrid *exceptPos); + bool isTerrainLoaded(const CellPreloader::PositionCellGrid &position, double referenceTime) const; private: Resource::ResourceSystem* mResourceSystem; @@ -88,7 +94,6 @@ namespace MWWorld bool mPreloadInstances; double mLastResourceCacheUpdate; - int mStoreViewsFailCount; struct PreloadEntry { @@ -114,6 +119,9 @@ namespace MWWorld std::vector mTerrainPreloadPositions; osg::ref_ptr mTerrainPreloadItem; osg::ref_ptr mUpdateCacheItem; + + std::vector mLoadedTerrainPositions; + double mLoadedTerrainTimestamp; }; } diff --git a/apps/openmw/mwworld/cellref.cpp b/apps/openmw/mwworld/cellref.cpp index 5e91ddcc67..0b16964043 100644 --- a/apps/openmw/mwworld/cellref.cpp +++ b/apps/openmw/mwworld/cellref.cpp @@ -48,11 +48,6 @@ namespace MWWorld return mCellRef.mRefID; } - const std::string* CellRef::getRefIdPtr() const - { - return &mCellRef.mRefID; - } - bool CellRef::getTeleport() const { return mCellRef.mTeleport; diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 6a6ac69c57..78170a766f 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -38,8 +38,8 @@ namespace MWWorld // Id of object being referenced std::string getRefId() const; - // Pointer to ID of the object being referenced - const std::string* getRefIdPtr() const; + // Reference to ID of the object being referenced + const std::string& getRefIdRef() const { return mCellRef.mRefID; } // For doors - true if this door teleports to somewhere else, false // if it should open through animation. diff --git a/apps/openmw/mwworld/cells.cpp b/apps/openmw/mwworld/cells.cpp index ad4dfcfb90..d020eace45 100644 --- a/apps/openmw/mwworld/cells.cpp +++ b/apps/openmw/mwworld/cells.cpp @@ -36,7 +36,7 @@ namespace } bool cont = cell.second.forEach([&] (MWWorld::Ptr ptr) { - if(*ptr.getCellRef().getRefIdPtr() == id) + if (ptr.getCellRef().getRefIdRef() == id) { return visitor(ptr); } diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 53245be247..b2ac511509 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -26,6 +26,7 @@ #include "../mwmechanics/recharge.hpp" #include "ptr.hpp" +#include "esmloader.hpp" #include "esmstore.hpp" #include "class.hpp" #include "containerstore.hpp" @@ -176,12 +177,19 @@ namespace if (state.mVersion < 15) fixRestocking(record, state); + if (state.mVersion < 17) + { + if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory); + else if constexpr (std::is_same_v) + MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats); + } if (state.mRef.mRefNum.hasContentFile()) { for (typename MWWorld::CellRefList::List::iterator iter (collection.mList.begin()); iter!=collection.mList.end(); ++iter) - if (iter->mRef.getRefNum()==state.mRef.mRefNum && *iter->mRef.getRefIdPtr() == state.mRef.mRefID) + if (iter->mRef.getRefNum()==state.mRef.mRefNum && iter->mRef.getRefIdRef() == state.mRef.mRefID) { // overwrite existing reference float oldscale = iter->mRef.getScale(); @@ -190,7 +198,7 @@ namespace const ESM::Position & newpos = iter->mData.getPosition(); const MWWorld::Ptr ptr(&*iter, cellstore); if ((oldscale != iter->mRef.getScale() || oldpos.asVec3() != newpos.asVec3() || oldpos.rot[0] != newpos.rot[0] || oldpos.rot[1] != newpos.rot[1] || oldpos.rot[2] != newpos.rot[2]) && !ptr.getClass().isActor()) - MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.pos[0], newpos.pos[1], newpos.pos[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, newpos.asVec3()); if (!iter->mData.isEnabled()) { iter->mData.enable(); @@ -417,7 +425,7 @@ namespace MWWorld const std::string *mIdToFind; bool operator()(const PtrType& ptr) { - if (*ptr.getCellRef().getRefIdPtr() == *mIdToFind) + if (ptr.getCellRef().getRefIdRef() == *mIdToFind) { mFound = ptr; return false; @@ -553,17 +561,12 @@ namespace MWWorld ESM::MovedCellRef cMRef; cMRef.mRefNum.mIndex = 0; bool deleted = false; - while(mCell->getNextRef(esm[index], ref, deleted, /*ignoreMoves*/true, &cMRef)) + bool moved = false; + while(mCell->getNextRef(esm[index], ref, deleted, cMRef, moved)) { - if (deleted) + if (deleted || moved) continue; - if (cMRef.mRefNum.mIndex) - { - cMRef.mRefNum.mIndex = 0; - continue; // ignore refs that are moved - } - // Don't list reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefNum); @@ -618,13 +621,11 @@ namespace MWWorld ESM::MovedCellRef cMRef; cMRef.mRefNum.mIndex = 0; bool deleted = false; - while(mCell->getNextRef(esm[index], ref, deleted, /*ignoreMoves*/true, &cMRef)) + bool moved = false; + while(mCell->getNextRef(esm[index], ref, deleted, cMRef, moved)) { - if (cMRef.mRefNum.mIndex) - { - cMRef.mRefNum.mIndex = 0; - continue; // ignore refs that are moved - } + if (moved) + continue; // Don't load reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = @@ -844,7 +845,12 @@ namespace MWWorld if (type == 0) { Log(Debug::Warning) << "Dropping reference to '" << cref.mRefID << "' (object no longer exists)"; - reader.skipHSubUntil("OBJE"); + // Skip until the next OBJE or MVRF + while(reader.hasMoreSubs() && !reader.peekNextSub("OBJE") && !reader.peekNextSub("MVRF")) + { + reader.getSubName(); + reader.skipHSub(); + } continue; } diff --git a/apps/openmw/mwworld/cellvisitors.hpp b/apps/openmw/mwworld/cellvisitors.hpp index fec8ca77b0..77f33fa84b 100644 --- a/apps/openmw/mwworld/cellvisitors.hpp +++ b/apps/openmw/mwworld/cellvisitors.hpp @@ -25,16 +25,6 @@ namespace MWWorld } }; - struct ListObjectsVisitor - { - std::vector mObjects; - - bool operator() (MWWorld::Ptr ptr) - { - mObjects.push_back (ptr); - return true; - } - }; } #endif diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 96f03af6c3..da4dd9d99e 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -30,12 +30,12 @@ namespace MWWorld } - void Class::insertObject(const Ptr& ptr, const std::string& mesh, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Class::insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { } - void Class::insertObjectPhysics(const Ptr& ptr, const std::string& mesh, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated) const + void Class::insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const {} bool Class::apply (const MWWorld::Ptr& ptr, const std::string& id, const MWWorld::Ptr& actor) const diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 7937209c6e..8e451ea580 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -78,9 +78,9 @@ namespace MWWorld } virtual void insertObjectRendering (const Ptr& ptr, const std::string& mesh, MWRender::RenderingInterface& renderingInterface) const; - virtual void insertObject(const Ptr& ptr, const std::string& mesh, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const; + virtual void insertObject(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; ///< Add reference into a cell for rendering (default implementation: don't render anything). - virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, osg::Quat rotation, MWPhysics::PhysicsSystem& physics, bool skipAnimated = false) const; + virtual void insertObjectPhysics(const Ptr& ptr, const std::string& mesh, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const; virtual std::string getName (const ConstPtr& ptr) const = 0; ///< \return name or ID; can return an empty string. diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index cced17688d..b02c2bb407 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -184,7 +184,7 @@ int MWWorld::ContainerStore::count(const std::string &id) const { int total=0; for (const auto&& iter : *this) - if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) + if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefIdRef(), id)) total += iter.getRefData().getCount(); return total; } @@ -249,7 +249,7 @@ bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const MWWorld::Class& cls1 = ptr1.getClass(); const MWWorld::Class& cls2 = ptr2.getClass(); - if (!Misc::StringUtils::ciEqual(ptr1.getCellRef().getRefId(), ptr2.getCellRef().getRefId())) + if (!Misc::StringUtils::ciEqual(ptr1.getCellRef().getRefIdRef(), ptr2.getCellRef().getRefIdRef())) return false; // If it has an enchantment, don't stack when some of the charge is already used @@ -364,7 +364,7 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp (const Ptr& ptr, for (MWWorld::ContainerStoreIterator iter (begin(type)); iter!=end(); ++iter) { - if (Misc::StringUtils::ciEqual((*iter).getCellRef().getRefId(), MWWorld::ContainerStore::sGoldId)) + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefIdRef(), MWWorld::ContainerStore::sGoldId)) { iter->getRefData().setCount(addItems(iter->getRefData().getCount(false), realCount)); flagAsModified(); @@ -465,7 +465,7 @@ int MWWorld::ContainerStore::remove(const std::string& itemId, int count, const int toRemove = count; for (ContainerStoreIterator iter(begin()); iter != end() && toRemove > 0; ++iter) - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), itemId)) + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefIdRef(), itemId)) toRemove -= remove(*iter, toRemove, actor, equipReplacement, resolveFirst); flagAsModified(); @@ -740,7 +740,7 @@ MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) for (auto&& iter : *this) { int iterHealth = iter.getClass().hasItemHealth(iter) ? iter.getClass().getItemHealth(iter) : 1; - if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefId(), id)) + if (Misc::StringUtils::ciEqual(iter.getCellRef().getRefIdRef(), id)) { // Prefer the stack with the lowest remaining uses // Try to get item with zero durability only if there are no other items found diff --git a/apps/openmw/mwworld/esmloader.cpp b/apps/openmw/mwworld/esmloader.cpp index b12d646e70..a01128fe36 100644 --- a/apps/openmw/mwworld/esmloader.cpp +++ b/apps/openmw/mwworld/esmloader.cpp @@ -2,6 +2,26 @@ #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 { @@ -28,4 +48,187 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index) mStore.load(mEsm[index], &mListener); } + 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 506105bebb..50631603de 100644 --- a/apps/openmw/mwworld/esmloader.hpp +++ b/apps/openmw/mwworld/esmloader.hpp @@ -13,6 +13,9 @@ namespace ToUTF8 namespace ESM { class ESMReader; + struct CreatureStats; + struct InventoryState; + struct NpcStats; } namespace MWWorld @@ -33,6 +36,8 @@ struct EsmLoader : public ContentLoader 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 4b9bdf7426..befe65d641 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -109,6 +109,21 @@ namespace return npcsToReplace; } + + // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no longer exists however. + // So instead of removing the item altogether, we're only removing the script. + template + void removeMissingScripts(const MWWorld::Store& scripts, std::map& items) + { + for(auto& [id, item] : items) + { + if(!item.mScript.empty() && !scripts.search(item.mScript)) + { + item.mScript.clear(); + Log(Debug::Verbose) << "Item '" << id << "' (" << item.mName << ") has nonexistent script '" << item.mScript << "', ignoring it."; + } + } + } } namespace MWWorld @@ -259,9 +274,9 @@ void ESMStore::countRecords() std::vector refs; std::vector refIDs; std::vector readers; - for(auto it = mCells.intBegin(); it != mCells.intEnd(); it++) + for(auto it = mCells.intBegin(); it != mCells.intEnd(); ++it) readRefs(*it, refs, refIDs, readers); - for(auto it = mCells.extBegin(); it != mCells.extEnd(); it++) + for(auto it = mCells.extBegin(); it != mCells.extEnd(); ++it) readRefs(*it, refs, refIDs, readers); const auto lessByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum); @@ -366,6 +381,33 @@ void ESMStore::validateDynamic() for (const ESM::NPC &npc : npcsToReplace) mNpcs.insert(npc); + + removeMissingScripts(mScripts, mArmors.mDynamic); + removeMissingScripts(mScripts, mBooks.mDynamic); + removeMissingScripts(mScripts, mClothes.mDynamic); + removeMissingScripts(mScripts, mWeapons.mDynamic); + + removeMissingObjects(mCreatureLists); + removeMissingObjects(mItemLists); +} + +// Leveled lists can be modified by scripts. This removes items that no longer exist (presumably because the plugin was removed) from modified lists +template +void ESMStore::removeMissingObjects(Store& store) +{ + for(auto& entry : store.mDynamic) + { + auto first = std::remove_if(entry.second.mList.begin(), entry.second.mList.end(), [&] (const auto& item) + { + if(!find(item.mId)) + { + Log(Debug::Verbose) << "Leveled list '" << entry.first << "' has nonexistent object '" << item.mId << "', ignoring it."; + return true; + } + return false; + }); + entry.second.mList.erase(first, entry.second.mList.end()); + } } int ESMStore::countSavedGameRecords() const diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 608b5489ea..6ad479f8bf 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -89,6 +89,9 @@ namespace MWWorld void validate(); void countRecords(); + + template + void removeMissingObjects(Store& store); public: /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index d96447f87d..49e60af1fd 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -108,11 +108,9 @@ MWWorld::InventoryStore::InventoryStore() MWWorld::InventoryStore::InventoryStore (const InventoryStore& store) : ContainerStore (store) - , mMagicEffects(store.mMagicEffects) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) - , mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes) , mSelectedEnchantItem(end()) { copySlots (store); @@ -125,9 +123,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor mListener = store.mListener; mInventoryListener = store.mInventoryListener; - mMagicEffects = store.mMagicEffects; mFirstAutoEquip = store.mFirstAutoEquip; - mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes; mRechargingItemsUpToDate = false; ContainerStore::operator= (store); mSlots.clear(); @@ -143,7 +139,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add(const Ptr& itemPtr, if (allowAutoEquip && actorPtr != MWMechanics::getPlayer() && actorPtr.getClass().isNpc() && !actorPtr.getClass().getNpcStats(actorPtr).isWerewolf()) { - std::string type = itemPtr.getTypeName(); + const std::string& type = itemPtr.getTypeName(); if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) autoEquip(actorPtr); } @@ -186,8 +182,6 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite flagAsModified(); fireEquipmentChangedEvent(actor); - - updateMagicEffects(actor); } void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) @@ -199,7 +193,6 @@ void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor) mUpdatesEnabled = true; fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot) @@ -554,7 +547,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor) { mSlots.swap (slots_); fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); flagAsModified(); } } @@ -567,129 +559,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(cons return slots[Slot_CarriedLeft]; } -const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const -{ - return mMagicEffects; -} - -void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor) -{ - // To avoid excessive updates during auto-equip - if (!mUpdatesEnabled) - return; - - // Delay update until the listener is set up - if (!mInventoryListener) - return; - - mMagicEffects = MWMechanics::MagicEffects(); - - const auto& stats = actor.getClass().getCreatureStats(actor); - if (stats.isDead() && stats.isDeathAnimationFinished()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector params; - - bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end()); - if (!existed) - { - params.resize(enchantment.mEffects.mList.size()); - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - int delta = effect.mMagnMax - effect.mMagnMin; - // Roll some dice, one for each effect - if (delta) - params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast(delta); - // Try resisting each effect - params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor); - ++i; - } - - // Note that using the RefID as a key here is not entirely correct. - // Consider equipping the same item twice (e.g. a ring) - // However, permanent enchantments with a random magnitude are kind of an exploit anyway, - // so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case. - mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params; - } - else - params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()]; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - const ESM::MagicEffect *magicEffect = - MWBase::Environment::get().getWorld()->getStore().get().find ( - effect.mEffectID); - - // Fully resisted or can't be applied to target? - if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer())) - { - i++; - continue; - } - - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (!existed) - { - // During first auto equip, we don't play any sounds. - // Basically we don't want sounds when the actor is first loaded, - // the items should appear as if they'd always been equipped. - mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip); - } - - if (magnitude) - mMagicEffects.add (effect, magnitude); - - i++; - } - } - } - - // Now drop expired effects - for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin(); - it != mPermanentMagicEffectMagnitudes.end();) - { - bool found = false; - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter == end()) - continue; - if ((**iter).getCellRef().getRefId() == it->first) - { - found = true; - } - } - if (!found) - mPermanentMagicEffectMagnitudes.erase(it++); - else - ++it; - } - - // Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping - MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); - - mFirstAutoEquip = false; -} - bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); @@ -748,7 +617,7 @@ int MWWorld::InventoryStore::remove(const Ptr& item, int count, const Ptr& actor if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { - std::string type = item.getTypeName(); + const std::string& type = item.getTypeName(); if (type == typeid(ESM::Armor).name() || type == typeid(ESM::Clothing).name()) autoEquip(actor); } @@ -800,7 +669,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c if (applyUpdates) { fireEquipmentChangedEvent(actor); - updateMagicEffects(actor); } return retval; @@ -848,7 +716,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con return unstack(item, actor, item.getRefData().getCount() - count); } -MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() +MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } @@ -856,7 +724,6 @@ MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor) { mInventoryListener = listener; - updateMagicEffects(actor); } void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) @@ -875,105 +742,6 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor) */ } -void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor) -{ - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - if (enchantmentId.empty()) - continue; - - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end()) - continue; - - int i=0; - for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList) - { - i++; - // Don't get spell icon display information for enchantments that weren't actually applied - if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0) - continue; - const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1]; - float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; - magnitude *= params.mMultiplier; - if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); - } - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) -{ - for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it) - { - if (*it != end()) - purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell); - } -} - -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) -{ - TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); - if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) - return; - - for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter) - { - if (*iter==end()) - continue; - - if ((*iter)->getCellRef().getRefId() != sourceId) - continue; - - std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter); - - if (!enchantmentId.empty()) - { - const ESM::Enchantment& enchantment = - *MWBase::Environment::get().getWorld()->getStore().get().find (enchantmentId); - - if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect) - continue; - - std::vector& params = effectMagnitudeIt->second; - - int i=0; - for (std::vector::const_iterator effectIt (enchantment.mEffects.mList.begin()); - effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i) - { - if (effectIt->mEffectID != effectId) - continue; - - if (effectIndex >= 0 && effectIndex != i) - continue; - - if (wholeSpell) - { - mPermanentMagicEffectMagnitudes.erase(sourceId); - return; - } - - float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom; - magnitude *= params[i].mMultiplier; - - if (magnitude) - mMagicEffects.add (*effectIt, -magnitude); - - params[i].mMultiplier = 0; - } - } - } -} - void MWWorld::InventoryStore::clear() { mSlots.clear(); @@ -991,38 +759,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item) return false; } -void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const +bool MWWorld::InventoryStore::isFirstEquip() { - MWWorld::ContainerStore::writeState(state); - - for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector > params; - for (std::vector::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - params.emplace_back(pIt->mRandom, pIt->mMultiplier); - } - - state.mPermanentMagicEffectMagnitudes[it->first] = params; - } -} - -void MWWorld::InventoryStore::readState(const ESM::InventoryState &state) -{ - MWWorld::ContainerStore::readState(state); - - for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin(); - it != state.mPermanentMagicEffectMagnitudes.end(); ++it) - { - std::vector params; - for (std::vector >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt) - { - EffectParams p; - p.mRandom = pIt->first; - p.mMultiplier = pIt->second; - params.push_back(p); - } - - mPermanentMagicEffectMagnitudes[it->first] = params; - } + bool first = mFirstAutoEquip; + mFirstAutoEquip = false; + return first; } diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 32dc0d2e91..01c53d7028 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -25,15 +25,6 @@ namespace MWWorld */ virtual void equipmentChanged () {} - /** - * @param effect - * @param isNew Is this effect new (e.g. the item for it was just now manually equipped) - * or was it loaded from a savegame / initial game state? \n - * If it isn't new, non-looping VFX should not be played. - * @param playSound Play effect sound? - */ - virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {} - virtual ~InventoryStoreListener() = default; }; @@ -68,8 +59,6 @@ namespace MWWorld private: - MWMechanics::MagicEffects mMagicEffects; - InventoryStoreListener* mInventoryListener; // Enables updates of magic effects and actor model whenever items are equipped or unequipped. @@ -78,19 +67,6 @@ namespace MWWorld bool mFirstAutoEquip; - // Vanilla allows permanent effects with a random magnitude, so it needs to be stored here. - // We also need this to only play sounds and particle effects when the item is equipped, rather than on every update. - struct EffectParams - { - // Modifier to scale between min and max magnitude - float mRandom; - // Multiplier for when an effect was fully or partially resisted - float mMultiplier; - }; - - typedef std::map > TEffectMagnitudes; - TEffectMagnitudes mPermanentMagicEffectMagnitudes; - typedef std::vector TSlots; TSlots mSlots; @@ -106,8 +82,6 @@ namespace MWWorld void initSlots (TSlots& slots_); - void updateMagicEffects(const Ptr& actor); - void fireEquipmentChangedEvent(const Ptr& actor); void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override; @@ -161,9 +135,6 @@ namespace MWWorld void autoEquip (const MWWorld::Ptr& actor); ///< Auto equip items according to stats and item value. - const MWMechanics::MagicEffects& getMagicEffects() const; - ///< Return magic effects from worn items. - bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override; ///< @return true if the two specified objects can stack with each other @@ -198,22 +169,12 @@ namespace MWWorld void setInvListener (InventoryStoreListener* listener, const Ptr& actor); ///< Set a listener for various events, see \a InventoryStoreListener - InventoryStoreListener* getInvListener(); - - void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor); - - void purgeEffect (short effectId, bool wholeSpell = false); - ///< Remove a magic effect - - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); - ///< Remove a magic effect + InventoryStoreListener* getInvListener() const; void clear() override; ///< Empty container. - void writeState (ESM::InventoryState& state) const override; - - void readState (const ESM::InventoryState& state) override; + bool isFirstEquip(); }; } diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index d8e2fb2f0d..caa0600f7c 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -22,9 +22,10 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" -#include "class.hpp" -#include "ptr.hpp" #include "cellstore.hpp" +#include "class.hpp" +#include "esmloader.hpp" +#include "ptr.hpp" namespace MWWorld { @@ -381,6 +382,14 @@ namespace MWWorld // this is the one object we can not silently drop. throw std::runtime_error ("invalid player state record (object state)"); } + if (reader.getFormat() < 17) + { + convertMagicEffects(player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); + for(std::size_t i = 0; i < ESM::Attribute::Length; ++i) + player.mSaveAttributes[i].mMod = 0.f; + for(std::size_t i = 0; i < ESM::Skill::Length; ++i) + player.mSaveSkills[i].mMod = 0.f; + } if (!player.mObject.mEnabled) { diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 439f5a4605..3b7be64ad1 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -238,9 +238,6 @@ namespace MWWorld state.mNode->addChild(projectileLightSource); projectileLightSource->setLight(projectileLight); } - - SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; - state.mNode->accept(disableFreezeOnCullVisitor); state.mNode->addCullCallback(new SceneUtil::LightListCallback); @@ -259,7 +256,7 @@ namespace MWWorld state.mEffectAnimationTime->addTime(duration); } - void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection) + void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection, int slot) { osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); if (caster.getClass().isActor()) @@ -281,6 +278,7 @@ namespace MWWorld MagicBoltState state; state.mSpellId = spellId; state.mCasterHandle = caster; + state.mSlot = slot; if (caster.getClass().isActor()) state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); else @@ -320,7 +318,7 @@ namespace MWWorld // in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape if (state.mIdMagic.size() > 1) model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get().find(state.mIdMagic[1])->mModel; - state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true, false); + state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true); state.mToDelete = false; mMagicBolts.push_back(state); } @@ -345,7 +343,7 @@ namespace MWWorld if (!ptr.getClass().getEnchantment(ptr).empty()) SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); - state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false, true); + state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false); state.mToDelete = false; mProjectiles.push_back(state); } @@ -496,9 +494,6 @@ namespace MWWorld auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); - if (const auto hitWaterPos = projectile->getWaterHitPosition()) - mRendering->emitWaterRipple(Misc::Convert::toOsg(*hitWaterPos)); - const auto pos = projectile->getPosition(); projectileState.mNode->setPosition(pos); @@ -522,9 +517,11 @@ namespace MWWorld if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId)) bow = *invIt; } + if (projectile->getHitWater()) + mRendering->emitWaterRipple(pos); MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength); - cleanupProjectile(projectileState); + projectileState.mToDelete = true; } for (auto& magicBoltState : mMagicBolts) { @@ -549,11 +546,23 @@ namespace MWWorld cast.mHitPosition = pos; cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; - cast.mStack = false; + cast.mSlot = magicBoltState.mSlot; cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true); - MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName); - cleanupMagicBolt(magicBoltState); + MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot); + magicBoltState.mToDelete = true; + } + + for (auto& projectileState : mProjectiles) + { + if (projectileState.mToDelete) + cleanupProjectile(projectileState); + } + + for (auto& magicBoltState : mMagicBolts) + { + if (magicBoltState.mToDelete) + cleanupMagicBolt(magicBoltState); } mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }), mProjectiles.end()); @@ -620,7 +629,7 @@ namespace MWWorld state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; - + state.mSlot = it->mSlot; state.mSpellId = it->mSpellId; state.mSpeed = it->mSpeed; @@ -654,7 +663,7 @@ namespace MWWorld int weaponType = ptr.get()->mBase->mData.mType; state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown; - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false, true); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false); } catch(...) { @@ -676,6 +685,7 @@ namespace MWWorld state.mSpellId = esm.mSpellId; state.mActorId = esm.mActorId; state.mToDelete = false; + state.mSlot = esm.mSlot; std::string texture; try @@ -707,7 +717,7 @@ namespace MWWorld osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture); - state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true, false); + state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); for (const std::string &soundid : state.mSoundIds) diff --git a/apps/openmw/mwworld/projectilemanager.hpp b/apps/openmw/mwworld/projectilemanager.hpp index 4dc250dc5f..f889250e1d 100644 --- a/apps/openmw/mwworld/projectilemanager.hpp +++ b/apps/openmw/mwworld/projectilemanager.hpp @@ -49,7 +49,7 @@ namespace MWWorld MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. - void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection); + void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot); void launchProjectile (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile, const osg::Vec3f& pos, const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength); @@ -107,6 +107,7 @@ namespace MWWorld ESM::EffectList mEffects; float mSpeed; + int mSlot; std::vector mSounds; std::set mSoundIds; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 7d3a6c7893..ed68a46059 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -84,7 +86,7 @@ namespace return rot; } - void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, osg::Quat rotation) + void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, const osg::Quat &rotation) { if (ptr.getRefData().getBaseNode()) rendering.rotateObject(ptr, rotation); @@ -92,19 +94,17 @@ namespace std::string getModel(const MWWorld::Ptr &ptr, const VFS::Manager *vfs) { + if (Misc::ResourceHelpers::isHiddenMarker(ptr.getCellRef().getRefId())) + return {}; bool useAnim = ptr.getClass().useAnim(); std::string model = ptr.getClass().getModel(ptr); if (useAnim) model = Misc::ResourceHelpers::correctActorModelPath(model, vfs); - - const std::string &id = ptr.getCellRef().getRefId(); - if (id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker") - model = ""; // marker objects that have a hardcoded function in the game logic, should be hidden from the player return model; } void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics, - MWRender::RenderingManager& rendering, std::set& pagedRefs, bool onlyPhysics) + MWRender::RenderingManager& rendering, std::set& pagedRefs) { if (ptr.getRefData().getBaseNode() || physics.getActor(ptr)) { @@ -114,12 +114,6 @@ namespace std::string model = getModel(ptr, rendering.getResourceSystem()->getVFS()); const auto rotation = makeNodeRotation(ptr, RotationOrder::direct); - if (onlyPhysics && !physics.getObject(ptr) && !ptr.getClass().isActor()) - { - // When we preload physics object we need to skip animated objects. They are dependant on the scene graph which doesn't yet exist. - ptr.getClass().insertObject (ptr, model, rotation, physics, true); - return; - } const ESM::RefNum& refnum = ptr.getCellRef().getRefNum(); if (!refnum.hasContentFile() || pagedRefs.find(refnum) == pagedRefs.end()) @@ -137,8 +131,7 @@ namespace // Restore effect particles MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr); - if (!physics.getObject(ptr)) - ptr.getClass().insertObject (ptr, model, rotation, physics); + ptr.getClass().insertObject (ptr, model, rotation, physics); MWBase::Environment::get().getLuaManager()->objectAddedToScene(ptr); } @@ -149,11 +142,9 @@ namespace { if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport()) { - const auto shape = object->getShapeInstance()->getCollisionShape(); - btVector3 aabbMin; btVector3 aabbMax; - shape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + object->getShapeInstance()->getCollisionShape()->getAabb(btTransform::getIdentity(), aabbMin, aabbMax); const auto center = (aabbMax + aabbMin) * 0.5f; @@ -180,12 +171,7 @@ namespace navigator.addObject( DetourNavigator::ObjectId(object), - DetourNavigator::DoorShapes( - *shape, - object->getShapeInstance()->getAvoidCollisionShape(), - connectionStart, - connectionEnd - ), + DetourNavigator::DoorShapes(object->getShapeInstance(), connectionStart, connectionEnd), transform ); } @@ -193,10 +179,7 @@ namespace { navigator.addObject( DetourNavigator::ObjectId(object), - DetourNavigator::ObjectShapes { - *object->getShapeInstance()->getCollisionShape(), - object->getShapeInstance()->getAvoidCollisionShape() - }, + DetourNavigator::ObjectShapes(object->getShapeInstance()), object->getTransform() ); } @@ -210,13 +193,11 @@ namespace struct InsertVisitor { MWWorld::CellStore& mCell; - Loading::Listener& mLoadingListener; - bool mOnlyObjects; - bool mTest; + Loading::Listener* mLoadingListener; std::vector mToInsert; - InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool onlyObjects, bool test); + InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener); bool operator() (const MWWorld::Ptr& ptr); @@ -224,8 +205,8 @@ namespace void insert(AddObject&& addObject); }; - InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool onlyObjects, bool test) - : mCell (cell), mLoadingListener (loadingListener), mOnlyObjects(onlyObjects), mTest(test) + InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener* loadingListener) + : mCell(cell), mLoadingListener(loadingListener) {} bool InsertVisitor::operator() (const MWWorld::Ptr& ptr) @@ -241,7 +222,7 @@ namespace { for (MWWorld::Ptr& ptr : mToInsert) { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled() && (!mOnlyObjects || !ptr.getClass().isActor())) + if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) { try { @@ -254,21 +235,11 @@ namespace } } - if (!mTest) - mLoadingListener.increaseProgress (1); + if (mLoadingListener != nullptr) + mLoadingListener->increaseProgress(1); } } - struct PositionVisitor - { - bool operator() (const MWWorld::Ptr& ptr) - { - if (!ptr.getRefData().isDeleted() && ptr.getRefData().isEnabled()) - ptr.getClass().adjustPosition (ptr, false); - return true; - } - }; - int getCellPositionDistanceToOrigin(const std::pair& cellPosition) { return std::abs(cellPosition.first) + std::abs(cellPosition.second); @@ -335,41 +306,13 @@ namespace MWWorld mRendering.update (duration, paused); } - void Scene::unloadInactiveCell (CellStore* cell, bool test) + void Scene::unloadCell(CellStore* cell) { - assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) != mInactiveCells.end()); - if (!test) - Log(Debug::Info) << "Unloading cell " << cell->getCell()->getDescription(); - - ListObjectsVisitor visitor; - - cell->forEach(visitor); - for (const auto& ptr : visitor.mObjects) - { - mPhysics->remove(ptr); - ptr.mRef->mData.mPhysicsPostponed = false; - } - - if (cell->getCell()->isExterior()) - { - const auto cellX = cell->getCell()->getGridX(); - const auto cellY = cell->getCell()->getGridY(); - mPhysics->removeHeightField(cellX, cellY); - } - - mInactiveCells.erase(cell); - } - - void Scene::deactivateCell(CellStore* cell, bool test) - { - assert(mInactiveCells.find(cell) != mInactiveCells.end()); if (mActiveCells.find(cell) == mActiveCells.end()) return; - if (!test) - Log(Debug::Info) << "Deactivate cell " << cell->getCell()->getDescription(); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); + Log(Debug::Info) << "Unloading cell " << cell->getCell()->getDescription(); + ListAndResetObjectsVisitor visitor; cell->forEach(visitor); @@ -378,13 +321,13 @@ namespace MWWorld { if (const auto object = mPhysics->getObject(ptr)) { - navigator->removeObject(DetourNavigator::ObjectId(object)); - if (object->isAnimated()) - mPhysics->remove(ptr); + mNavigator.removeObject(DetourNavigator::ObjectId(object)); + mPhysics->remove(ptr); + ptr.mRef->mData.mPhysicsPostponed = false; } else if (mPhysics->getActor(ptr)) { - navigator->removeAgent(world->getPathfindingHalfExtents(ptr)); + mNavigator.removeAgent(world->getPathfindingHalfExtents(ptr)); mRendering.removeActorPath(ptr); mPhysics->remove(ptr); } @@ -396,18 +339,20 @@ namespace MWWorld if (cell->getCell()->isExterior()) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->removeObject(DetourNavigator::ObjectId(heightField)); + if (mPhysics->getHeightField(cellX, cellY) != nullptr) + mNavigator.removeHeightfield(osg::Vec2i(cellX, cellY)); + + mPhysics->removeHeightField(cellX, cellY); } if (cell->getCell()->hasWater()) - navigator->removeWater(osg::Vec2i(cellX, cellY)); + mNavigator.removeWater(osg::Vec2i(cellX, cellY)); if (const auto pathgrid = world->getStore().get().search(*cell->getCell())) - navigator->removePathgrid(*pathgrid); + mNavigator.removePathgrid(*pathgrid); const auto player = world->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + mNavigator.update(player.getRefData().getPosition().asVec3()); MWBase::Environment::get().getMechanicsManager()->drop (cell); @@ -420,107 +365,26 @@ namespace MWWorld mActiveCells.erase(cell); } - void Scene::activateCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test) + void Scene::loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn) { + using DetourNavigator::HeightfieldShape; + assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) != mInactiveCells.end()); mActiveCells.insert(cell); - if (test) - Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription(); - else - Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); + Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription(); const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); const int cellX = cell->getCell()->getGridX(); const int cellY = cell->getCell()->getGridY(); - if (!test && cell->getCell()->isExterior()) + if (cell->getCell()->isExterior()) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->addObject(DetourNavigator::ObjectId(heightField), *heightField->getShape(), - heightField->getCollisionObject()->getWorldTransform()); - } - - if (const auto pathgrid = world->getStore().get().search(*cell->getCell())) - navigator->addPathgrid(*cell->getCell(), *pathgrid); - - // register local scripts - // do this before insertCell, to make sure we don't add scripts from levelled creature spawning twice - MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); - - if (respawn) - cell->respawn(); - - insertCell (*cell, loadingListener, false, test); - - mRendering.addCell(cell); - if (!test) - { - MWBase::Environment::get().getWindowManager()->addCell(cell); - bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); - float waterLevel = cell->getWaterLevel(); - mRendering.setWaterEnabled(waterEnabled); - if (waterEnabled) - { - mPhysics->enableWater(waterLevel); - mRendering.setWaterHeight(waterLevel); - - if (cell->getCell()->isExterior()) - { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) - navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, - cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform()); - } - else - { - navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), - cell->getWaterLevel(), btTransform::getIdentity()); - } - } - else - mPhysics->disableWater(); - - const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - - // By default the player is grounded, with the scene fully loaded, we validate and correct this. - if (player.mCell == cell) // Only run once, during initial cell load. - { - mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f); - } - - navigator->update(player.getRefData().getPosition().asVec3()); - - if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) - mRendering.configureAmbient(cell->getCell()); - } - - mPreloader->notifyLoaded(cell); - } - - void Scene::loadInactiveCell (CellStore *cell, Loading::Listener* loadingListener, bool test) - { - assert(mActiveCells.find(cell) == mActiveCells.end()); - assert(mInactiveCells.find(cell) == mInactiveCells.end()); - mInactiveCells.insert(cell); - - if (test) - Log(Debug::Info) << "Testing inactive cell " << cell->getCell()->getDescription(); - else - Log(Debug::Info) << "Loading inactive cell " << cell->getCell()->getDescription(); - - if (!test && cell->getCell()->isExterior()) - { - float verts = ESM::Land::LAND_SIZE; - float worldsize = ESM::Land::REAL_SIZE; - - const int cellX = cell->getCell()->getGridX(); - const int cellY = cell->getCell()->getGridY(); - 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; if (data) { mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get()); @@ -531,21 +395,98 @@ namespace MWWorld 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()); } + if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + { + const osg::Vec2i cellPosition(cellX, cellY); + const btVector3& origin = heightField->getCollisionObject()->getWorldTransform().getOrigin(); + const osg::Vec3f shift(origin.x(), origin.y(), origin.z()); + const HeightfieldShape shape = [&] () -> HeightfieldShape + { + if (data == nullptr) + { + return DetourNavigator::HeightfieldPlane {static_cast(ESM::Land::DEFAULT_HEIGHT)}; + } + else + { + DetourNavigator::HeightfieldSurface heights; + heights.mHeights = data->mHeights; + heights.mSize = static_cast(ESM::Land::LAND_SIZE); + heights.mMinHeight = data->mMinHeight; + heights.mMaxHeight = data->mMaxHeight; + return heights; + } + } (); + mNavigator.addHeightfield(cellPosition, ESM::Land::REAL_SIZE, shift, shape); + } } - insertCell (*cell, loadingListener, true, test); + if (const auto pathgrid = world->getStore().get().search(*cell->getCell())) + mNavigator.addPathgrid(*cell->getCell(), *pathgrid); + + // register local scripts + // do this before insertCell, to make sure we don't add scripts from levelled creature spawning twice + MWBase::Environment::get().getWorld()->getLocalScripts().addCell (cell); + + if (respawn) + cell->respawn(); + + insertCell(*cell, loadingListener); + + mRendering.addCell(cell); + + MWBase::Environment::get().getWindowManager()->addCell(cell); + bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior(); + float waterLevel = cell->getWaterLevel(); + mRendering.setWaterEnabled(waterEnabled); + if (waterEnabled) + { + mPhysics->enableWater(waterLevel); + mRendering.setWaterHeight(waterLevel); + + if (cell->getCell()->isExterior()) + { + if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + { + 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)); + } + } + else + { + mNavigator.addWater(osg::Vec2i(cellX, cellY), std::numeric_limits::max(), + osg::Vec3f(0, 0, waterLevel)); + } + } + else + mPhysics->disableWater(); + + const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); + + // The player is loaded before the scene and by default it is grounded, with the scene fully loaded, we validate and correct this. + if (player.mCell == cell) // Only run once, during initial cell load. + { + mPhysics->traceDown(player, player.getRefData().getPosition().asVec3(), 10.f); + } + + mNavigator.update(player.getRefData().getPosition().asVec3()); + + if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx)) + mRendering.configureAmbient(cell->getCell()); + + mPreloader->notifyLoaded(cell); } void Scene::clear() { - for (auto iter = mInactiveCells.begin(); iter!=mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) { auto* cell = *iter++; - deactivateCell(cell); - unloadInactiveCell (cell); + unloadCell (cell); } assert(mActiveCells.empty()); - assert(mInactiveCells.empty()); mCurrentCell = nullptr; mPreloader->clear(); @@ -574,9 +515,8 @@ namespace MWWorld void Scene::playerMoved(const osg::Vec3f &pos) { - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->updatePlayerPosition(player.getRefData().getPosition().asVec3()); + mNavigator.updatePlayerPosition(player.getRefData().getPosition().asVec3()); if (!mCurrentCell || !mCurrentCell->isExterior()) return; @@ -588,7 +528,7 @@ namespace MWWorld void Scene::changeCellGrid (const osg::Vec3f &pos, int playerCellX, int playerCellY, bool changeEvent) { - for (auto iter = mInactiveCells.begin(); iter != mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter != mActiveCells.end(); ) { auto* cell = *iter++; if (cell->getCell()->isExterior()) @@ -596,23 +536,20 @@ namespace MWWorld const auto dx = std::abs(playerCellX - cell->getCell()->getGridX()); const auto dy = std::abs(playerCellY - cell->getCell()->getGridY()); if (dx > mHalfGridSize || dy > mHalfGridSize) - deactivateCell(cell); - - if (dx > mHalfGridSize+1 || dy > mHalfGridSize+1) - unloadInactiveCell(cell); + unloadCell(cell); } else - { - deactivateCell(cell); - unloadInactiveCell(cell); - } + unloadCell (cell); } mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); mRendering.setActiveGrid(newGrid); - preloadTerrain(pos, true); + if (mRendering.pagingUnlockCache()) + mPreloader->abortTerrainPreloadExcept(nullptr); + if (!mPreloader->isTerrainLoaded(std::make_pair(pos, newGrid), mRendering.getReferenceTime())) + preloadTerrain(pos, true); mPagedRefs.clear(); mRendering.getPagedRefnums(newGrid, mPagedRefs); @@ -652,7 +589,6 @@ namespace MWWorld } auto cellsPositionsToLoad = cellsToLoad(mActiveCells,mHalfGridSize); - auto cellsPositionsToLoadInactive = cellsToLoad(mInactiveCells,mHalfGridSize+1); Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); @@ -675,26 +611,12 @@ namespace MWWorld return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); }); - std::sort(cellsPositionsToLoadInactive.begin(), cellsPositionsToLoadInactive.end(), - [&] (const std::pair& lhs, const std::pair& rhs) { - return getCellPositionPriority(lhs) < getCellPositionPriority(rhs); - }); - - // Load cells - for (const auto& [x,y] : cellsPositionsToLoadInactive) - { - if (!isCellInCollection(x, y, mInactiveCells)) - { - CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - loadInactiveCell (cell, loadingListener); - } - } for (const auto& [x,y] : cellsPositionsToLoad) { if (!isCellInCollection(x, y, mActiveCells)) { CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y); - activateCell (cell, loadingListener, changeEvent); + loadCell (cell, loadingListener, changeEvent); } } @@ -726,20 +648,16 @@ namespace MWWorld { loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")..."); - CellStoreCollection::iterator iter = mActiveCells.begin(); - CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY); - loadInactiveCell (cell, loadingListener, true); - activateCell (cell, loadingListener, false, true); + loadCell(cell, nullptr, false); - iter = mActiveCells.begin(); + auto iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() && it->mData.mY == (*iter)->getCell()->getGridY()) { - deactivateCell(*iter, true); - unloadInactiveCell (*iter, true); + unloadCell(*iter); break; } @@ -777,18 +695,16 @@ namespace MWWorld loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")..."); CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName); - loadInactiveCell (cell, loadingListener, true); - activateCell (cell, loadingListener, false, true); + loadCell(cell, nullptr, false); - CellStoreCollection::iterator iter = mActiveCells.begin(); + auto iter = mActiveCells.begin(); while (iter != mActiveCells.end()) { assert (!(*iter)->getCell()->isExterior()); if (it->mName == (*iter)->getCell()->mName) { - deactivateCell(*iter, true); - unloadInactiveCell (*iter, true); + unloadCell(*iter); break; } @@ -820,12 +736,8 @@ namespace MWWorld mRendering.updatePlayerPtr(player); if (adjustPlayerPos) { - world->moveObject(player, pos.pos[0], pos.pos[1], pos.pos[2]); - - float x = pos.rot[0]; - float y = pos.rot[1]; - float z = pos.rot[2]; - world->rotateObject(player, x, y, z); + world->moveObject(player, pos.asVec3()); + world->rotateObject(player, pos.asRotationVec3()); player.getClass().adjustPosition(player, true); } @@ -867,6 +779,11 @@ namespace MWWorld Scene::~Scene() { + for (const osg::ref_ptr& v : mWorkItems) + v->abort(); + + for (const osg::ref_ptr& v : mWorkItems) + v->waitTillDone(); } bool Scene::hasCellChanged() const @@ -894,12 +811,8 @@ namespace MWWorld if(mCurrentCell != nullptr && *mCurrentCell == *cell) { MWBase::World *world = MWBase::Environment::get().getWorld(); - world->moveObject(world->getPlayerPtr(), position.pos[0], position.pos[1], position.pos[2]); - - float x = position.rot[0]; - float y = position.rot[1]; - float z = position.rot[2]; - world->rotateObject(world->getPlayerPtr(), x, y, z); + world->moveObject(world->getPlayerPtr(), position.asVec3()); + world->rotateObject(world->getPlayerPtr(), position.asRotationVec3()); if (adjustPlayerPos) world->getPlayerPtr().getClass().adjustPosition(world->getPlayerPtr(), true); @@ -910,21 +823,18 @@ namespace MWWorld Log(Debug::Info) << "Changing to interior"; // unload - for (auto iter = mInactiveCells.begin(); iter!=mInactiveCells.end(); ) + for (auto iter = mActiveCells.begin(); iter!=mActiveCells.end(); ) { - auto* cell = *iter++; - deactivateCell(cell); - unloadInactiveCell(cell); + auto* cellToUnload = *iter++; + unloadCell(cellToUnload); } assert(mActiveCells.empty()); - assert(mInactiveCells.empty()); loadingListener->setProgressRange(cell->count()); // Load cell. mPagedRefs.clear(); - loadInactiveCell (cell, loadingListener); - activateCell (cell, loadingListener, changeEvent); + loadCell(cell, loadingListener, changeEvent); changePlayerCell(cell, position, adjustPlayerPos); @@ -974,31 +884,23 @@ namespace MWWorld mCellChanged = false; } - void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool onlyObjects, bool test) + void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener) { - InsertVisitor insertVisitor (cell, *loadingListener, onlyObjects, test); + InsertVisitor insertVisitor(cell, loadingListener); cell.forEach (insertVisitor); - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs, onlyObjects); }); - if (!onlyObjects) - { - insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); - - // do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order - PositionVisitor posVisitor; - cell.forEach (posVisitor); - } + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering, mPagedRefs); }); + insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); }); } void Scene::addObjectToScene (const Ptr& ptr) { try { - addObject(ptr, *mPhysics, mRendering, mPagedRefs, false); + addObject(ptr, *mPhysics, mRendering, mPagedRefs); addObject(ptr, *mPhysics, mNavigator); MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale()); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + mNavigator.update(player.getRefData().getPosition().asVec3()); } catch (std::exception& e) { @@ -1006,21 +908,20 @@ namespace MWWorld } } - void Scene::removeObjectFromScene (const Ptr& ptr) + void Scene::removeObjectFromScene (const Ptr& ptr, bool keepActive) { - MWBase::Environment::get().getMechanicsManager()->remove (ptr); + MWBase::Environment::get().getMechanicsManager()->remove (ptr, keepActive); MWBase::Environment::get().getSoundManager()->stopSound3D (ptr); MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); - const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); if (const auto object = mPhysics->getObject(ptr)) { - navigator->removeObject(DetourNavigator::ObjectId(object)); + mNavigator.removeObject(DetourNavigator::ObjectId(object)); const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr(); - navigator->update(player.getRefData().getPosition().asVec3()); + mNavigator.update(player.getRefData().getPosition().asVec3()); } else if (mPhysics->getActor(ptr)) { - navigator->removeAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); + mNavigator.removeAgent(MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(ptr)); } mPhysics->remove(ptr); mRendering.removeObject (ptr); @@ -1061,6 +962,9 @@ namespace MWWorld void doWork() override { + if (mAborted) + return; + try { mSceneManager->getTemplate(mMesh); @@ -1069,9 +973,16 @@ namespace MWWorld { } } + + void abort() override + { + mAborted = true; + } + private: std::string mMesh; Resource::SceneManager* mSceneManager; + std::atomic_bool mAborted {false}; }; void Scene::preload(const std::string &mesh, bool useAnim) @@ -1081,7 +992,13 @@ namespace MWWorld mesh_ = Misc::ResourceHelpers::correctActorModelPath(mesh_, mRendering.getResourceSystem()->getVFS()); if (!mRendering.getResourceSystem()->getSceneManager()->checkLoaded(mesh_, mRendering.getReferenceTime())) - mRendering.getWorkQueue()->addWorkItem(new PreloadMeshItem(mesh_, mRendering.getResourceSystem()->getSceneManager())); + { + osg::ref_ptr item(new PreloadMeshItem(mesh_, mRendering.getResourceSystem()->getSceneManager())); + mRendering.getWorkQueue()->addWorkItem(item); + const auto isDone = [] (const osg::ref_ptr& v) { return v->isDone(); }; + mWorkItems.erase(std::remove_if(mWorkItems.begin(), mWorkItems.end(), isDone), mWorkItems.end()); + mWorkItems.emplace_back(std::move(item)); + } } void Scene::preloadCells(float dt) @@ -1216,32 +1133,16 @@ namespace MWWorld { std::vector vec; vec.emplace_back(pos, gridCenterToBounds(getNewGridCenter(pos))); - if (sync && mRendering.pagingUnlockCache()) - mPreloader->abortTerrainPreloadExcept(nullptr); - else - mPreloader->abortTerrainPreloadExcept(&vec[0]); + mPreloader->abortTerrainPreloadExcept(&vec[0]); mPreloader->setTerrainPreloadPositions(vec); if (!sync) return; Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); Loading::ScopedLoad load(loadingListener); - int progress = 0, initialProgress = -1, progressRange = 0; - while (!mPreloader->syncTerrainLoad(vec, progress, progressRange, mRendering.getReferenceTime())) - { - if (initialProgress == -1) - { - loadingListener->setLabel("#{sLoadingMessage4}"); - initialProgress = progress; - } - if (progress) - { - loadingListener->setProgressRange(std::max(0, progressRange-initialProgress)); - loadingListener->setProgress(progress-initialProgress); - } - else - loadingListener->setProgress(0); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } + + loadingListener->setLabel("#{sLoadingMessage4}"); + + while (!mPreloader->syncTerrainLoad(vec, mRendering.getReferenceTime(), *loadingListener)) {} } void Scene::reloadTerrain() diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index bc9c2386bb..623bddbc82 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "ptr.hpp" #include "globals.hpp" @@ -10,6 +11,7 @@ #include #include #include +#include #include @@ -49,6 +51,11 @@ namespace MWPhysics class PhysicsSystem; } +namespace SceneUtil +{ + class WorkItem; +} + namespace MWWorld { class Player; @@ -70,7 +77,6 @@ namespace MWWorld CellStore* mCurrentCell; // the cell the player is in CellStoreCollection mActiveCells; - CellStoreCollection mInactiveCells; bool mCellChanged; MWPhysics::PhysicsSystem *mPhysics; MWRender::RenderingManager& mRendering; @@ -91,7 +97,9 @@ namespace MWWorld std::set mPagedRefs; - void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool onlyObjects, bool test = false); + std::vector> mWorkItems; + + void insertCell(CellStore &cell, Loading::Listener* loadingListener); osg::Vec2i mCurrentGridCenter; // Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center @@ -107,10 +115,8 @@ namespace MWWorld osg::Vec4i gridCenterToBounds(const osg::Vec2i ¢erCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f &pos, const osg::Vec2i *currentGridCenter = nullptr) const; - void unloadInactiveCell (CellStore* cell, bool test = false); - void deactivateCell (CellStore* cell, bool test = false); - void activateCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false); - void loadInactiveCell (CellStore *cell, Loading::Listener* loadingListener, bool test = false); + void unloadCell(CellStore* cell); + void loadCell(CellStore *cell, Loading::Listener* loadingListener, bool respawn); public: @@ -152,7 +158,7 @@ namespace MWWorld void addObjectToScene (const Ptr& ptr); ///< Add an object that already exists in the world model to the scene. - void removeObjectFromScene (const Ptr& ptr); + void removeObjectFromScene (const Ptr& ptr, bool keepActive = false); ///< Remove an object from the scene, but not from the world model. void removeFromPagedRefs(const Ptr &ptr); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index 2ed051a141..718cebd790 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -11,25 +11,6 @@ #include #include -namespace -{ - struct Compare - { - bool operator()(const ESM::Land *x, const ESM::Land *y) { - if (x->mX == y->mX) { - return x->mY < y->mY; - } - return x->mX < y->mX; - } - bool operator()(const ESM::Land *x, const std::pair& y) { - if (x->mX == y.first) { - return x->mY < y.second; - } - return x->mX < y.first; - } - }; -} - namespace MWWorld { RecordId::RecordId(const std::string &id, bool isDeleted) @@ -412,11 +393,6 @@ namespace MWWorld //========================================================================= Store::~Store() { - for (const ESM::Land* staticLand : mStatic) - { - delete staticLand; - } - } size_t Store::getSize() const { @@ -433,13 +409,8 @@ namespace MWWorld const ESM::Land *Store::search(int x, int y) const { std::pair comp(x,y); - - std::vector::const_iterator it = - std::lower_bound(mStatic.begin(), mStatic.end(), comp, Compare()); - - if (it != mStatic.end() && (*it)->mX == x && (*it)->mY == y) { - return *it; - } + if (auto it = mStatic.find(comp); it != mStatic.end() && it->mX == x && it->mY == y) + return &*it; return nullptr; } const ESM::Land *Store::find(int x, int y) const @@ -454,25 +425,19 @@ namespace MWWorld } RecordId Store::load(ESM::ESMReader &esm) { - ESM::Land *ptr = new ESM::Land(); + ESM::Land land; bool isDeleted = false; - ptr->load(esm, isDeleted); + land.load(esm, isDeleted); // Same area defined in multiple plugins? -> last plugin wins - // Can't use search() because we aren't sorted yet - is there any other way to speed this up? - for (std::vector::iterator it = mStatic.begin(); it != mStatic.end(); ++it) - { - if ((*it)->mX == ptr->mX && (*it)->mY == ptr->mY) - { - delete *it; - mStatic.erase(it); - break; - } + auto [it, inserted] = mStatic.insert(std::move(land)); + if (!inserted) { + auto nh = mStatic.extract(it); + nh.value() = std::move(land); + mStatic.insert(std::move(nh)); } - mStatic.push_back(ptr); - return RecordId("", isDeleted); } void Store::setUp() @@ -481,7 +446,6 @@ namespace MWWorld if (mBuilt) return; - std::sort(mStatic.begin(), mStatic.end(), Compare()); mBuilt = true; } @@ -502,8 +466,8 @@ namespace MWWorld { ESM::CellRef ref; ESM::MovedCellRef cMRef; - cMRef.mRefNum.mIndex = 0; bool deleted = false; + bool moved = false; ESM::ESM_Context ctx = esm.getContext(); @@ -512,10 +476,10 @@ namespace MWWorld // // Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following // implementation when the oher implementation works as well. - while (cell->getNextRef(esm, ref, deleted, /*ignoreMoves*/true, &cMRef)) + while (cell->getNextRef(esm, ref, deleted, cMRef, moved)) { - if (!cMRef.mRefNum.mIndex) - continue; // ignore refs that are not moved + if (!moved) + continue; ESM::Cell *cellAlt = const_cast(searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1])); @@ -556,7 +520,8 @@ namespace MWWorld const ESM::Cell *Store::search(int x, int y) const { ESM::Cell cell; - cell.mData.mX = x, cell.mData.mY = y; + cell.mData.mX = x; + cell.mData.mY = y; std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); @@ -574,7 +539,8 @@ namespace MWWorld const ESM::Cell *Store::searchStatic(int x, int y) const { ESM::Cell cell; - cell.mData.mX = x, cell.mData.mY = y; + cell.mData.mX = x; + cell.mData.mY = y; std::pair key(x, y); DynamicExt::const_iterator it = mExt.find(key); @@ -909,6 +875,26 @@ namespace MWWorld // A proper fix should be made for future versions of the file format. bool interior = pathgrid.mData.mX == 0 && pathgrid.mData.mY == 0 && mCells->search(pathgrid.mCell) != nullptr; + // deal with mods that have empty pathgrid records (Issue #6209) + // we assume that these records are empty on purpose (i.e. to remove old pathgrid on an updated cell) + if (isDeleted || pathgrid.mPoints.empty() || pathgrid.mEdges.empty()) + { + if (interior) + { + Interior::iterator it = mInt.find(pathgrid.mCell); + if (it != mInt.end()) + mInt.erase(it); + } + else + { + Exterior::iterator it = mExt.find(std::make_pair(pathgrid.mData.mX, pathgrid.mData.mY)); + if (it != mExt.end()) + mExt.erase(it); + } + + return RecordId("", isDeleted); + } + // Try to overwrite existing record if (interior) { diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index e37152431e..4b1d648703 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include "recordcmp.hpp" @@ -74,10 +76,10 @@ namespace MWWorld const T *find(int index) const; }; - template + template > class SharedIterator { - typedef typename std::vector::const_iterator Iter; + typedef typename Container::const_iterator Iter; Iter mIter; @@ -233,10 +235,28 @@ namespace MWWorld template <> class Store : public StoreBase { - std::vector mStatic; + struct SpatialComparator + { + using is_transparent = void; + + bool operator()(const ESM::Land& x, const ESM::Land& y) const + { + return std::tie(x.mX, x.mY) < std::tie(y.mX, y.mY); + } + bool operator()(const ESM::Land& x, const std::pair& y) const + { + return std::tie(x.mX, x.mY) < std::tie(y.first, y.second); + } + bool operator()(const std::pair& x, const ESM::Land& y) const + { + return std::tie(x.first, x.second) < std::tie(y.mX, y.mY); + } + }; + using Statics = std::set; + Statics mStatic; public: - typedef SharedIterator iterator; + typedef typename Statics::iterator iterator; virtual ~Store(); @@ -408,7 +428,7 @@ namespace MWWorld const ESM::WeaponType *search(const int id) const; const ESM::WeaponType *find(const int id) const; - RecordId load(ESM::ESMReader &esm) override { return RecordId(nullptr, false); } + RecordId load(ESM::ESMReader &esm) override { return RecordId({}, false); } ESM::WeaponType* insert(const ESM::WeaponType &weaponType); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 8fea737d8d..299cc8676b 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -76,21 +77,6 @@ #include "contentloader.hpp" #include "esmloader.hpp" -namespace -{ - -// Wraps a value to (-PI, PI] -void wrap(float& rad) -{ - const float pi = static_cast(osg::PI); - if (rad>0) - rad = std::fmod(rad+pi, 2.0f*pi)-pi; - else - rad = std::fmod(rad-pi, 2.0f*pi)+pi; -} - -} - namespace MWWorld { struct GameContentLoader : public ContentLoader @@ -1128,18 +1114,12 @@ namespace MWWorld } } - MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, float x, float y, float z, bool movePhysics) + MWWorld::Ptr World::moveObject(const Ptr &ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics, bool keepActive) { ESM::Position pos = ptr.getRefData().getPosition(); - - pos.pos[0] = x; - pos.pos[1] = y; - pos.pos[2] = z; - + std::memcpy(pos.pos, &position, sizeof(osg::Vec3f)); ptr.getRefData().setPosition(pos); - osg::Vec3f vec(x, y, z); - CellStore *currCell = ptr.isInCell() ? ptr.getCell() : nullptr; // currCell == nullptr should only happen for player, during initial startup bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = isPlayer || (currCell && mWorldScene->isCellActive(*currCell)); @@ -1179,7 +1159,8 @@ namespace MWWorld if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); - mWorldScene->addObjectToScene(newPtr); + if(newPtr.getRefData().isEnabled()) + mWorldScene->addObjectToScene(newPtr); std::string script = newPtr.getClass().getScript(newPtr); if (!script.empty()) @@ -1190,7 +1171,7 @@ namespace MWWorld } else if (!newCellActive && currCellActive) { - mWorldScene->removeObjectFromScene(ptr); + mWorldScene->removeObjectFromScene(ptr, keepActive); mLocalScripts.remove(ptr); removeContainerScripts (ptr); haveToMove = false; @@ -1228,7 +1209,7 @@ namespace MWWorld } if (haveToMove && newPtr.getRefData().getBaseNode()) { - mWorldScene->updateObjectPosition(newPtr, vec, movePhysics); + mWorldScene->updateObjectPosition(newPtr, position, movePhysics); if (movePhysics) { if (const auto object = mPhysics->getObject(ptr)) @@ -1237,7 +1218,7 @@ namespace MWWorld } if (isPlayer) - mWorldScene->playerMoved(vec); + mWorldScene->playerMoved(position); else { mRendering->pagingBlacklistObject(mStore.find(ptr.getCellRef().getRefId()), ptr); @@ -1247,10 +1228,10 @@ namespace MWWorld return newPtr; } - MWWorld::Ptr World::moveObject (const Ptr& ptr, float x, float y, float z, bool movePhysics, bool moveToActive) + MWWorld::Ptr World::moveObject(const Ptr& ptr, const osg::Vec3f& position, bool movePhysics, bool moveToActive) { int cellX, cellY; - positionToIndex(x, y, cellX, cellY); + positionToIndex(position.x(), position.y(), cellX, cellY); CellStore* cell = ptr.getCell(); CellStore* newCell = getExterior(cellX, cellY); @@ -1259,18 +1240,18 @@ namespace MWWorld if (cell->isExterior() || (moveToActive && isCellActive && ptr.getClass().isActor())) cell = newCell; - return moveObject(ptr, cell, x, y, z, movePhysics); + return moveObject(ptr, cell, position, movePhysics); } - MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) + MWWorld::Ptr World::moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) { auto* actor = mPhysics->getActor(ptr); osg::Vec3f newpos = ptr.getRefData().getPosition().asVec3() + vec; if (actor) actor->adjustPosition(vec, ignoreCollisions); if (ptr.getClass().isActor()) - return moveObject(ptr, newpos.x(), newpos.y(), newpos.z(), false, moveToActive && ptr != getPlayerPtr()); - return moveObject(ptr, newpos.x(), newpos.y(), newpos.z()); + return moveObject(ptr, newpos, false, moveToActive && ptr != getPlayerPtr()); + return moveObject(ptr, newpos); } void World::scaleObject (const Ptr& ptr, float scale) @@ -1294,10 +1275,8 @@ namespace MWWorld updateNavigatorObject(*object); } - void World::rotateObjectImp(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) + void World::rotateObject(const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags) { - const float pi = static_cast(osg::PI); - ESM::Position pos = ptr.getRefData().getPosition(); float *objRot = pos.rot; if (flags & MWBase::RotationFlag_adjust) @@ -1319,13 +1298,9 @@ namespace MWWorld * currently it's done so for rotating the camera, which needs * clamping. */ - const float half_pi = pi/2.f; - - if(objRot[0] < -half_pi) objRot[0] = -half_pi; - else if(objRot[0] > half_pi) objRot[0] = half_pi; - - wrap(objRot[1]); - wrap(objRot[2]); + objRot[0] = osg::clampBetween(objRot[0], -osg::PIf / 2, osg::PIf / 2); + objRot[1] = Misc::normalizeAngle(objRot[1]); + objRot[2] = Misc::normalizeAngle(objRot[2]); } ptr.getRefData().setPosition(pos); @@ -1377,7 +1352,7 @@ namespace MWWorld pos.z() = std::min(pos.z(), traced.z()); } - moveObject(ptr, ptr.getCell(), pos.x(), pos.y(), pos.z()); + moveObject(ptr, ptr.getCell(), pos); } void World::fixPosition() @@ -1416,12 +1391,7 @@ namespace MWWorld } } - void World::rotateObject (const Ptr& ptr, float x, float y, float z, MWBase::RotationFlags flags) - { - rotateObjectImp(ptr, osg::Vec3f(x, y, z), flags); - } - - void World::rotateWorldObject (const Ptr& ptr, osg::Quat rotate) + void World::rotateWorldObject (const Ptr& ptr, const osg::Quat& rotate) { if(ptr.getRefData().getBaseNode() != nullptr) { @@ -1436,7 +1406,7 @@ namespace MWWorld } } - MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) + MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) { return copyObjectToCell(ptr,cell,pos,ptr.getRefData().getCount(),false); } @@ -1519,35 +1489,12 @@ namespace MWWorld void World::doPhysics(float duration, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { - mPhysics->stepSimulation(); processDoors(duration); - mProjectileManager->update(duration); - - const auto& results = mPhysics->applyQueuedMovement(duration, mDiscardMovements, frameStart, frameNumber, stats); + mPhysics->stepSimulation(duration, mDiscardMovements, frameStart, frameNumber, stats); mProjectileManager->processHits(); mDiscardMovements = false; - - for(const auto& actor : results) - { - // Handle player last, in case a cell transition occurs - if(actor != getPlayerPtr()) - { - auto* physactor = mPhysics->getActor(actor); - assert(physactor); - const auto position = physactor->getSimulationPosition(); - moveObject(actor, position.x(), position.y(), position.z(), false, false); - } - } - - const auto player = std::find(results.begin(), results.end(), getPlayerPtr()); - if (player != results.end()) - { - auto* physactor = mPhysics->getActor(*player); - assert(physactor); - const auto position = physactor->getSimulationPosition(); - moveObject(*player, position.x(), position.y(), position.z(), false, false); - } + mPhysics->moveActors(); } void World::updateNavigator() @@ -1567,10 +1514,7 @@ namespace MWWorld void World::updateNavigatorObject(const MWPhysics::Object& object) { - const DetourNavigator::ObjectShapes shapes { - *object.getShapeInstance()->getCollisionShape(), - object.getShapeInstance()->getAvoidCollisionShape() - }; + const DetourNavigator::ObjectShapes shapes(object.getShapeInstance()); mShouldUpdateNavigator = mNavigator->updateObject(DetourNavigator::ObjectId(&object), shapes, object.getTransform()) || mShouldUpdateNavigator; } @@ -1604,14 +1548,16 @@ namespace MWWorld bool World::rotateDoor(const Ptr door, MWWorld::DoorState state, float duration) { const ESM::Position& objPos = door.getRefData().getPosition(); - float oldRot = objPos.rot[2]; + auto oldRot = objPos.asRotationVec3(); + auto newRot = oldRot; float minRot = door.getCellRef().getPosition().rot[2]; float maxRot = minRot + osg::DegreesToRadians(90.f); float diff = duration * osg::DegreesToRadians(90.f) * (state == MWWorld::DoorState::Opening ? 1 : -1); - float targetRot = std::min(std::max(minRot, oldRot + diff), maxRot); - rotateObject(door, objPos.rot[0], objPos.rot[1], targetRot, MWBase::RotationFlag_none); + float targetRot = std::clamp(oldRot.z() + diff, minRot, maxRot); + newRot.z() = targetRot; + rotateObject(door, newRot, MWBase::RotationFlag_none); bool reached = (targetRot == maxRot && state != MWWorld::DoorState::Idle) || targetRot == minRot; @@ -1662,7 +1608,7 @@ namespace MWWorld MWBase::Environment::get().getSoundManager()->stopSound3D(door, closeSound); } - rotateObject(door, objPos.rot[0], objPos.rot[1], oldRot, MWBase::RotationFlag_none); + rotateObject(door, oldRot, MWBase::RotationFlag_none); } return reached; @@ -1916,7 +1862,7 @@ namespace MWWorld mRendering->getCamera()->setSneakOffset(0.f); int blind = 0; - auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects(); + 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))); @@ -2279,7 +2225,7 @@ namespace MWWorld pos.pos[0] -= adjust.x(); pos.pos[1] -= adjust.y(); pos.pos[2] -= adjust.z(); - moveObject(dropped, pos.pos[0], pos.pos[1], pos.pos[2]); + moveObject(dropped, pos.asVec3()); } } @@ -2487,7 +2433,7 @@ namespace MWWorld else { // Remove the old CharacterController - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); mNavigator->removeAgent(getPathfindingHalfExtents(getPlayerConstPtr())); mPhysics->remove(getPlayerPtr()); mRendering->removePlayer(getPlayerPtr()); @@ -2503,7 +2449,7 @@ namespace MWWorld void World::renderPlayer() { - MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr()); + MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr(), true); MWWorld::Ptr player = getPlayerPtr(); @@ -2513,7 +2459,7 @@ namespace MWWorld player.getClass().getInventoryStore(player).setContListener(anim); scaleObject(player, player.getCellRef().getScale()); // apply race height - rotateObject(player, 0.f, 0.f, 0.f, MWBase::RotationFlag_inverseOrder | MWBase::RotationFlag_adjust); + rotateObject(player, osg::Vec3f(), MWBase::RotationFlag_inverseOrder | MWBase::RotationFlag_adjust); MWBase::Environment::get().getMechanicsManager()->add(getPlayerPtr()); MWBase::Environment::get().getWindowManager()->watchActor(getPlayerPtr()); @@ -3163,7 +3109,20 @@ namespace MWWorld { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (inv.getSelectedEnchantItem() != inv.end()) - cast.cast(*inv.getSelectedEnchantItem()); + { + const auto& itemPtr = *inv.getSelectedEnchantItem(); + auto [slots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + int slot = 0; + for(std::size_t i = 0; i < slots.size(); ++i) + { + if(inv.getSlot(slots[i]) == inv.getSelectedEnchantItem()) + { + slot = slots[i]; + break; + } + } + cast.cast(itemPtr, slot); + } } } @@ -3180,6 +3139,7 @@ namespace MWWorld bool underwater = MWBase::Environment::get().getWorld()->isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); if (underwater) { + MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); mRendering->emitWaterRipple(worldPos); return; } @@ -3197,9 +3157,9 @@ namespace MWWorld mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } - void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) + void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) { - mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection); + mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection, slot); } void World::updateProjectilesCasters() @@ -3207,46 +3167,24 @@ namespace MWWorld mProjectileManager->updateCasters(); } - class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor - { - private: - MWWorld::Ptr mActor; - - public: - ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor) - : mActor(actor) - { - } - - void visit (MWMechanics::EffectKey key, int /*effectIndex*/, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, - float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override - { - const ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); - const auto magicEffect = store.get().find(key.mId); - if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0) - return; - const ESM::Static* castStatic; - if (!magicEffect->mHit.empty()) - castStatic = store.get().find (magicEffect->mHit); - else - castStatic = store.get().find ("VFX_DefaultHit"); - MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor); - if (anim && !castStatic->mModel.empty()) - anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle); - } - }; - void World::applyLoopingParticles(const MWWorld::Ptr& ptr) { const MWWorld::Class &cls = ptr.getClass(); if (cls.isActor()) { - ApplyLoopingParticlesVisitor visitor(ptr); - cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor); - cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor); - if (cls.hasInventoryStore(ptr)) - cls.getInventoryStore(ptr).visitEffectSources(visitor); + std::set playing; + for(const auto& params : cls.getCreatureStats(ptr).getActiveSpells()) + { + for(const auto& effect : params.getEffects()) + { + if(playing.insert(effect.mEffectId).second) + { + const auto magicEffect = getStore().get().find(effect.mEffectId); + if(magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) + MWMechanics::playEffects(ptr, *magicEffect, false); + } + } + } } } @@ -3257,10 +3195,7 @@ namespace MWWorld void World::breakInvisibility(const Ptr &actor) { - actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility); - actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility); - if (actor.getClass().hasInventoryStore(actor)) - actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility); + actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(actor, ESM::MagicEffect::Invisibility); // Normally updated once per frame, but here it is kinda important to do it right away. MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor); @@ -3753,7 +3688,7 @@ namespace MWWorld } void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, - const std::string& id, const std::string& sourceName, const bool fromProjectile) + const std::string& id, const std::string& sourceName, const bool fromProjectile, int slot) { std::map > toApply; for (const ESM::ENAMstruct& effectInfo : effects.mList) @@ -3831,7 +3766,7 @@ namespace MWWorld cast.mHitPosition = origin; cast.mId = id; cast.mSourceName = sourceName; - cast.mStack = false; + cast.mSlot = slot; ESM::EffectList effectsToApply; effectsToApply.mList = applyPair.second; cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true); @@ -3859,8 +3794,8 @@ namespace MWWorld return true; const ESM::Position& origPos = ptr.getCellRef().getPosition(); - MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.pos[0], origPos.pos[1], origPos.pos[2]); - MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.rot[0], origPos.rot[1], origPos.rot[2]); + MWBase::Environment::get().getWorld()->moveObject(ptr, origPos.asVec3()); + MWBase::Environment::get().getWorld()->rotateObject(ptr, origPos.asRotationVec3()); ptr.getClass().adjustPosition(ptr, true); } return true; @@ -3988,9 +3923,10 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const + bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); + return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 87cd14dd5c..6e48f045c0 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -133,8 +133,6 @@ namespace MWWorld void updateWeather(float duration, bool paused = false); - void rotateObjectImp (const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags); - Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos); void updateSoundListener(); @@ -226,7 +224,7 @@ namespace MWWorld void setWaterHeight(const float height) override; - void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) override; + void rotateWorldObject (const MWWorld::Ptr& ptr, const osg::Quat& rotate) override; bool toggleWater() override; bool toggleWorld() override; @@ -372,13 +370,13 @@ namespace MWWorld void undeleteObject (const Ptr& ptr) override; - MWWorld::Ptr moveObject (const Ptr& ptr, float x, float y, float z, bool movePhysics=true, bool moveToActive=false) override; + MWWorld::Ptr moveObject (const Ptr& ptr, const osg::Vec3f& position, bool movePhysics=true, bool moveToActive=false) override; ///< @return an updated Ptr in case the Ptr's cell changes - MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, float x, float y, float z, bool movePhysics=true) override; + MWWorld::Ptr moveObject (const Ptr& ptr, CellStore* newCell, const osg::Vec3f& position, bool movePhysics=true, bool keepActive=false) override; ///< @return an updated Ptr - MWWorld::Ptr moveObjectBy(const Ptr& ptr, osg::Vec3f vec, bool moveToActive, bool ignoreCollisions) override; + MWWorld::Ptr moveObjectBy(const Ptr& ptr, const osg::Vec3f& vec, bool moveToActive, bool ignoreCollisions) override; ///< @return an updated Ptr void scaleObject (const Ptr& ptr, float scale) override; @@ -387,10 +385,9 @@ namespace MWWorld /// @note Rotations via this method use a different rotation order than the initial rotations in the CS. This /// could be considered a bug, but is needed for MW compatibility. /// \param adjust indicates rotation should be set or adjusted - void rotateObject (const Ptr& ptr, float x, float y, float z, - MWBase::RotationFlags flags = MWBase::RotationFlag_inverseOrder) override; + void rotateObject (const Ptr& ptr, const osg::Vec3f& rot, MWBase::RotationFlags flags = MWBase::RotationFlag_inverseOrder) override; - MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, ESM::Position pos) override; + MWWorld::Ptr placeObject(const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) override; ///< Place an object. Makes a copy of the Ptr. MWWorld::Ptr safePlaceObject (const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) override; @@ -639,7 +636,7 @@ namespace MWWorld */ void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; - void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; + void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) override; void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override; void updateProjectilesCasters() override; @@ -684,7 +681,7 @@ namespace MWWorld void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, - const bool fromProjectile=false) override; + const bool fromProjectile=false, int slot = 0) override; void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override; @@ -738,7 +735,8 @@ namespace MWWorld bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, + const MWWorld::ConstPtr& ignore, std::vector* occupyingActors) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index b0fec1a1a3..62d3f9de04 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -4,6 +4,10 @@ #include #include #include +#include +#include + +#include #include #include @@ -14,6 +18,7 @@ #include #include +#include MATCHER_P3(Vec3fEq, x, y, z, "") { @@ -38,6 +43,10 @@ namespace float mStepSize; AreaCosts mAreaCosts; Loading::Listener mListener; + const osg::Vec2i mCellPosition {0, 0}; + const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1); + const osg::Vec3f mShift {0, 0, 0}; + const float mEndTolerance = 0; DetourNavigatorNavigatorTest() : mPlayerPosition(0, 0, 0) @@ -80,19 +89,54 @@ namespace }; template - btHeightfieldTerrainShape makeSquareHeightfieldTerrainShape(const std::array& values, + std::unique_ptr makeSquareHeightfieldTerrainShape(const std::array& values, btScalar heightScale = 1, int upAxis = 2, PHY_ScalarType heightDataType = PHY_FLOAT, bool flipQuadEdges = false) { const int width = static_cast(std::sqrt(size)); const btScalar min = *std::min_element(values.begin(), values.end()); const btScalar max = *std::max_element(values.begin(), values.end()); const btScalar greater = std::max(std::abs(min), std::abs(max)); - return btHeightfieldTerrainShape(width, width, values.data(), heightScale, -greater, greater, upAxis, heightDataType, flipQuadEdges); + return std::make_unique(width, width, values.data(), heightScale, -greater, greater, + upAxis, heightDataType, flipQuadEdges); } + template + HeightfieldSurface makeSquareHeightfieldSurface(const std::array& values) + { + const auto [min, max] = std::minmax_element(values.begin(), values.end()); + const float greater = std::max(std::abs(*min), std::abs(*max)); + HeightfieldSurface surface; + surface.mHeights = values.data(); + surface.mMinHeight = -greater; + surface.mMaxHeight = greater; + surface.mSize = static_cast(std::sqrt(size)); + return surface; + } + + template + osg::ref_ptr makeBulletShapeInstance(std::unique_ptr&& shape) + { + osg::ref_ptr bulletShape(new Resource::BulletShape); + bulletShape->mCollisionShape = std::move(shape).release(); + return new Resource::BulletShapeInstance(bulletShape); + } + + template + class CollisionShapeInstance + { + public: + CollisionShapeInstance(std::unique_ptr&& shape) : mInstance(makeBulletShapeInstance(std::move(shape))) {} + + T& shape() { return static_cast(*mInstance->mCollisionShape); } + const osg::ref_ptr& instance() const { return mInstance; } + + private: + osg::ref_ptr mInstance; + }; + TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -100,7 +144,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, mOut), + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } @@ -109,28 +153,28 @@ namespace mNavigator->addAgent(mAgentHalfExtents); mNavigator->addAgent(mAgentHalfExtents); mNavigator->removeAgent(mAgentHalfExtents); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::StartPolygonNotFound); } TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { - const std::array heightfieldData {{ + constexpr std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204.0000152587890625, 204, 1.99998295307159423828125), @@ -160,26 +204,25 @@ namespace TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - btBoxShape boxShape(btVector3(20, 20, 100)); - btCompoundShape compoundShape; - compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape); + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -206,13 +249,14 @@ namespace Vec3fEq(204, -204, 1.99998295307159423828125) )) << mPath; - mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); 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, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -243,27 +287,26 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - btBoxShape boxShape(btVector3(20, 20, 100)); - btCompoundShape compoundShape; - compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape); + CollisionShapeInstance compound(std::make_unique()); + compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), new btBoxShape(btVector3(20, 20, 100))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->addObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -291,15 +334,16 @@ namespace Vec3fEq(204, -204, 1.99998295307159423828125) )) << mPath; - compoundShape.updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); + compound.shape().updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0))); - mNavigator->updateObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity()); + mNavigator->updateObject(ObjectId(&compound.shape()), ObjectShapes(compound.instance()), btTransform::getIdentity()); 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, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -327,17 +371,17 @@ namespace )) << mPath; } - TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_should_use_higher) + TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) { - const std::array heightfieldData {{ + const std::array heightfieldData1 {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1)); + heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); const std::array heightfieldData2 {{ -25, -25, -25, -25, -25, @@ -346,16 +390,17 @@ namespace -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, }}; - btHeightfieldTerrainShape shape2 = makeSquareHeightfieldTerrainShape(heightfieldData2); - shape2.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield2(makeSquareHeightfieldTerrainShape(heightfieldData2)); + heightfield2.shape().setLocalScaling(btVector3(128, 128, 1)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape2), shape2, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield1.shape()), ObjectShapes(heightfield1.instance()), btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield2.shape()), ObjectShapes(heightfield2.instance()), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.999981403350830078125), @@ -383,8 +428,35 @@ namespace )) << mPath; } + TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) + { + const std::array heightfieldData1 {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); + + const std::array heightfieldData2 {{ + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + -25, -25, -25, -25, -25, + }}; + const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2); + + 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)); + } + TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape) { + osg::ref_ptr bulletShape(new Resource::BulletShape); + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, @@ -392,8 +464,9 @@ namespace 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); + shapePtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mCollisionShape = shapePtr.release(); std::array heightfieldDataAvoid {{ -25, -25, -25, -25, -25, @@ -402,15 +475,19 @@ namespace -25, -25, -25, -25, -25, -25, -25, -25, -25, -25, }}; - btHeightfieldTerrainShape shapeAvoid = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); - shapeAvoid.setLocalScaling(btVector3(128, 128, 1)); + std::unique_ptr shapeAvoidPtr = makeSquareHeightfieldTerrainShape(heightfieldDataAvoid); + shapeAvoidPtr->setLocalScaling(btVector3(128, 128, 1)); + bulletShape->mAvoidCollisionShape = shapeAvoidPtr.release(); + + osg::ref_ptr instance(new Resource::BulletShapeInstance(bulletShape)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), ObjectShapes {shape, &shapeAvoid}, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(instance->getCollisionShape()), ObjectShapes(instance), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99997997283935546875), @@ -441,19 +518,18 @@ namespace TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_ground_lower_than_water_with_only_swim_flag) { - std::array heightfieldData {{ + std::array heightfieldData {{ -50, -50, -50, -50, 0, -50, -100, -150, -100, -50, -50, -150, -200, -150, -100, -50, -100, -150, -100, -100, 0, -50, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, 300, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, 300)); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -462,7 +538,8 @@ namespace mEnd.x() = 0; mEnd.z() = 300; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(0, 204, 185.33331298828125), @@ -486,7 +563,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_swim_and_walk_flags) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -495,19 +572,18 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addWater(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), osg::Vec3f(0, 0, -25)); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); mStart.x() = 0; mEnd.x() = 0; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( @@ -532,7 +608,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_max_int_cells_size_and_swim_and_walk_flags) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -541,19 +617,18 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); - mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits::max(), -25, btTransform::getIdentity()); + 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->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); mStart.x() = 0; mEnd.x() = 0; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mOut), + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre( @@ -578,7 +653,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_ground_when_ground_cross_water_with_only_walk_flag) { - std::array heightfieldData {{ + std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, 0, 0, -100, -100, -100, -100, -100, 0, 0, -100, -150, -150, -150, -100, 0, @@ -587,19 +662,19 @@ namespace 0, -100, -100, -100, -100, -100, 0, 0, 0, 0, 0, 0, 0, 0, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, osg::Vec3f(0, 0, -25)); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); mStart.x() = 0; mEnd.x() = 0; - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(0, 204, -98.000030517578125), @@ -622,7 +697,7 @@ namespace )) << mPath; } - TEST_F(DetourNavigatorNavigatorTest, update_remove_and_update_then_find_path_should_return_path) + TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) { const std::array heightfieldData {{ 0, 0, 0, 0, 0, @@ -631,23 +706,77 @@ namespace 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData)); + heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->removeObject(ObjectId(&shape)); + mNavigator->removeObject(ObjectId(&heightfield.shape())); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addObject(ObjectId(&heightfield.shape()), ObjectShapes(heightfield.instance()), btTransform::getIdentity()); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); - EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(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) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + + mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + mNavigator->removeHeightfield(mCellPosition); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -677,18 +806,17 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -709,40 +837,41 @@ namespace mSettings.mAsyncNavMeshUpdaterThreads = 2; mNavigator.reset(new NavigatorImpl(mSettings)); - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const std::vector boxShapes(100, btVector3(20, 20, 100)); + std::vector> boxes; + std::generate_n(std::back_inserter(boxes), 100, [] { return std::make_unique(btVector3(20, 20, 100)); }); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); - for (std::size_t i = 0; i < boxShapes.size(); ++i) + for (std::size_t i = 0; i < boxes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10, i * 10, i * 10)); - mNavigator->addObject(ObjectId(&boxShapes[i]), boxShapes[i], transform); + mNavigator->addObject(ObjectId(&boxes[i].shape()), ObjectShapes(boxes[i].instance()), transform); } std::this_thread::sleep_for(std::chrono::microseconds(1)); - for (std::size_t i = 0; i < boxShapes.size(); ++i) + for (std::size_t i = 0; i < boxes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 10 + 1, i * 10 + 1, i * 10 + 1)); - mNavigator->updateObject(ObjectId(&boxShapes[i]), boxShapes[i], transform); + 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, mOut), Status::Success); + EXPECT_EQ(mNavigator->findPath(mAgentHalfExtents, mStepSize, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); EXPECT_THAT(mPath, ElementsAre( Vec3fEq(-204, 204, 1.99998295307159423828125), @@ -773,14 +902,15 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_multiple_times_object_should_delay_navmesh_change) { - const std::vector shapes(100, btVector3(64, 64, 64)); + std::vector> shapes; + std::generate_n(std::back_inserter(shapes), 100, [] { return std::make_unique(btVector3(64, 64, 64)); }); mNavigator->addAgent(mAgentHalfExtents); for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32, i * 32, i * 32)); - mNavigator->addObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->addObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform); } mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -789,7 +919,7 @@ namespace for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 1, i * 32 + 1, i * 32 + 1)); - mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform); } mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -797,7 +927,7 @@ namespace for (std::size_t i = 0; i < shapes.size(); ++i) { const btTransform transform(btMatrix3x3::getIdentity(), btVector3(i * 32 + 2, i * 32 + 2, i * 32 + 2)); - mNavigator->updateObject(ObjectId(&shapes[i]), shapes[i], transform); + mNavigator->updateObject(ObjectId(&shapes[i].shape()), ObjectShapes(shapes[i].instance()), transform); } mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -810,18 +940,17 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape shape = makeSquareHeightfieldTerrainShape(heightfieldData); - shape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity()); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, surface); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -833,26 +962,25 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) { - const std::array heightfieldData {{ + const std::array heightfieldData {{ 0, 0, 0, 0, 0, 0, -25, -25, -25, -25, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, 0, -25, -100, -100, -100, }}; - btHeightfieldTerrainShape heightfieldShape = makeSquareHeightfieldTerrainShape(heightfieldData); - heightfieldShape.setLocalScaling(btVector3(128, 128, 1)); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); - const btBoxShape oscillatingBoxShape(btVector3(20, 20, 20)); + CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); const btVector3 oscillatingBoxShapePosition(32, 32, 400); - const btBoxShape boderBoxShape(btVector3(50, 50, 50)); + CollisionShapeInstance boderBox(std::make_unique(btVector3(50, 50, 50))); mNavigator->addAgent(mAgentHalfExtents); - mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity()); - mNavigator->addObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * (surface.mSize - 1), mShift, 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(&boderBoxShape), boderBoxShape, + mNavigator->addObject(ObjectId(&boderBox.shape()), ObjectShapes(boderBox.instance()), btTransform(btMatrix3x3::getIdentity(), oscillatingBoxShapePosition + btVector3(0, 0, 200))); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); @@ -869,7 +997,7 @@ namespace { const btTransform transform(btQuaternion(btVector3(0, 0, 1), n * 2 * osg::PI / 10), oscillatingBoxShapePosition); - mNavigator->updateObject(ObjectId(&oscillatingBoxShape), oscillatingBoxShape, transform); + mNavigator->updateObject(ObjectId(&oscillatingBox.shape()), ObjectShapes(oscillatingBox.instance()), transform); mNavigator->update(mPlayerPosition); mNavigator->wait(mListener, WaitConditionType::allJobsDone); } @@ -881,4 +1009,136 @@ namespace ASSERT_EQ(navMesh->getNavMeshRevision(), 4); } } + + TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield) + { + const HeightfieldPlane plane {100}; + + mNavigator->addAgent(mAgentHalfExtents); + mNavigator->addHeightfield(mCellPosition, mHeightfieldTileSize * 4, mShift, plane); + mNavigator->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::requiredTilesPresent); + + EXPECT_EQ(mNavigator->findPath(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) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + + 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->update(mPlayerPosition); + mNavigator->wait(mListener, WaitConditionType::allJobsDone); + + EXPECT_EQ(mNavigator->findPath(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) + )) << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) + { + const std::array heightfieldData {{ + 0, 0, 0, 0, 0, + 0, -25, -25, -25, -25, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + 0, -25, -100, -100, -100, + }}; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + + 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->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), + 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) + )) << mPath; + } } diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp index 447a5b44e4..309266142f 100644 --- a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp +++ b/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp @@ -3,46 +3,156 @@ #include #include #include +#include +#include +#include +#include -#include +#include + +#include #include -namespace DetourNavigator -{ - static inline bool operator ==(const NavMeshDataRef& lhs, const NavMeshDataRef& rhs) - { - return std::make_pair(lhs.mValue, lhs.mSize) == std::make_pair(rhs.mValue, rhs.mSize); - } -} +#include +#include namespace { using namespace testing; using namespace DetourNavigator; + void* permRecastAlloc(int size) + { + void* result = rcAlloc(static_cast(size), RC_ALLOC_PERM); + if (result == nullptr) + throw std::bad_alloc(); + return result; + } + + template + void generate(T*& values, int size) + { + values = static_cast(permRecastAlloc(size * sizeof(T))); + std::generate_n(values, static_cast(size), [] { return static_cast(std::rand()); }); + } + + void generate(rcPolyMesh& value, int size) + { + value.nverts = size; + value.maxpolys = size; + value.nvp = size; + value.npolys = size; + rcVcopy(value.bmin, osg::Vec3f(-1, -2, -3).ptr()); + rcVcopy(value.bmax, osg::Vec3f(3, 2, 1).ptr()); + value.cs = 1.0f / (std::rand() % 999 + 1); + value.ch = 1.0f / (std::rand() % 999 + 1); + value.borderSize = std::rand(); + value.maxEdgeError = 1.0f / (std::rand() % 999 + 1); + generate(value.verts, getVertsLength(value)); + generate(value.polys, getPolysLength(value)); + generate(value.regs, getRegsLength(value)); + generate(value.flags, getFlagsLength(value)); + generate(value.areas, getAreasLength(value)); + } + + void generate(rcPolyMeshDetail& value, int size) + { + value.nmeshes = size; + value.nverts = size; + value.ntris = size; + generate(value.meshes, getMeshesLength(value)); + generate(value.verts, getVertsLength(value)); + generate(value.tris, getTrisLength(value)); + } + + void generate(PreparedNavMeshData& value, int size) + { + value.mUserId = std::rand(); + value.mCellHeight = 1.0f / (std::rand() % 999 + 1); + value.mCellSize = 1.0f / (std::rand() % 999 + 1); + generate(value.mPolyMesh, size); + generate(value.mPolyMeshDetail, size); + } + + std::unique_ptr makePeparedNavMeshData(int size) + { + auto result = std::make_unique(); + generate(*result, size); + return result; + } + + template + void clone(const T* src, T*& dst, std::size_t size) + { + dst = static_cast(permRecastAlloc(static_cast(size) * sizeof(T))); + std::memcpy(dst, src, size * sizeof(T)); + } + + void clone(const rcPolyMesh& src, rcPolyMesh& dst) + { + dst.nverts = src.nverts; + dst.npolys = src.npolys; + dst.maxpolys = src.maxpolys; + dst.nvp = src.nvp; + rcVcopy(dst.bmin, src.bmin); + rcVcopy(dst.bmax, src.bmax); + dst.cs = src.cs; + dst.ch = src.ch; + dst.borderSize = src.borderSize; + dst.maxEdgeError = src.maxEdgeError; + clone(src.verts, dst.verts, getVertsLength(dst)); + clone(src.polys, dst.polys, getPolysLength(dst)); + clone(src.regs, dst.regs, getRegsLength(dst)); + clone(src.flags, dst.flags, getFlagsLength(dst)); + clone(src.areas, dst.areas, getAreasLength(dst)); + } + + void clone(const rcPolyMeshDetail& src, rcPolyMeshDetail& dst) + { + dst.nmeshes = src.nmeshes; + dst.nverts = src.nverts; + dst.ntris = src.ntris; + clone(src.meshes, dst.meshes, getMeshesLength(dst)); + clone(src.verts, dst.verts, getVertsLength(dst)); + clone(src.tris, dst.tris, getTrisLength(dst)); + } + + std::unique_ptr clone(const PreparedNavMeshData& value) + { + auto result = std::make_unique(); + result->mUserId = value.mUserId; + result->mCellHeight = value.mCellHeight; + result->mCellSize = value.mCellSize; + clone(value.mPolyMesh, result->mPolyMesh); + clone(value.mPolyMeshDetail, result->mPolyMeshDetail); + return result; + } + + Mesh makeMesh() + { + std::vector indices {{0, 1, 2}}; + std::vector vertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}}; + std::vector areaTypes {1, AreaType_ground}; + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + struct DetourNavigatorNavMeshTilesCacheTest : Test { const osg::Vec3f mAgentHalfExtents {1, 2, 3}; const TilePosition mTilePosition {0, 0}; const std::size_t mGeneration = 0; const std::size_t mRevision = 0; - const std::vector mIndices {{0, 1, 2}}; - const std::vector mVertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}}; - const std::vector mAreaTypes {1, AreaType_ground}; - const std::vector mWater {}; - const RecastMesh mRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, mWater}; - const std::vector mOffMeshConnections {}; - unsigned char* const mData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mNavMeshData {mData, 1}; + const Mesh mMesh {makeMesh()}; + 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 size_t cRecastMeshKeySize = mRecastMesh.getIndices().size() * sizeof(int) - + mRecastMesh.getVertices().size() * sizeof(float) - + mRecastMesh.getAreaTypes().size() * sizeof(AreaType) - + mRecastMesh.getWater().size() * sizeof(RecastMesh::Water) - + mOffMeshConnections.size() * sizeof(OffMeshConnection); - - const size_t cRecastMeshWithWaterKeySize = cRecastMeshKeySize + sizeof(RecastMesh::Water); + const std::size_t mRecastMeshSize = sizeof(mRecastMesh) + getSize(mRecastMesh); + const std::size_t mRecastMeshWithWaterSize = mRecastMeshSize + sizeof(Cell); + const std::size_t mPreparedNavMeshDataSize = sizeof(*mPreparedNavMeshData) + getSize(*mPreparedNavMeshData); }; TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_empty_cache_should_return_empty_value) @@ -50,7 +160,7 @@ namespace const std::size_t maxSize = 0; NavMeshTilesCache cache(maxSize); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_for_not_enought_cache_size_should_return_empty_value) @@ -58,51 +168,46 @@ namespace const std::size_t maxSize = 0; NavMeshTilesCache cache(maxSize); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData))); - EXPECT_NE(mNavMeshData.mValue, nullptr); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData))); + EXPECT_NE(mPreparedNavMeshData, nullptr); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_return_cached_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); + ASSERT_EQ(*mPreparedNavMeshData, *copy); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + EXPECT_EQ(result.get(), *copy); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_return_cached_element) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + auto copy = clone(*mPreparedNavMeshData); + const auto sameCopy = clone(*mPreparedNavMeshData); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_EQ(mNavMeshData.mValue, nullptr); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_EQ(mPreparedNavMeshData, nullptr); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(copy)); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + EXPECT_EQ(result.get(), *sameCopy); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto result = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + EXPECT_EQ(result.get(), *copy); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_agent_half_extents_should_return_empty_value) @@ -111,8 +216,8 @@ namespace NavMeshTilesCache cache(maxSize); const osg::Vec3f unexsistentAgentHalfExtents {1, 1, 1}; - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(unexsistentAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(unexsistentAgentHalfExtents, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_tile_position_should_return_empty_value) @@ -121,228 +226,201 @@ namespace NavMeshTilesCache cache(maxSize); const TilePosition unexistentTilePosition {1, 1}; - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(mAgentHalfExtents, unexistentTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentHalfExtents, unexistentTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_recast_mesh_should_return_empty_value) { const std::size_t maxSize = 1; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh unexistentRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh unexistentRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh, mOffMeshConnections)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); + const auto copy = clone(*anotherPreparedNavMeshData); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, + std::move(anotherPreparedNavMeshData)); ASSERT_TRUE(result); - EXPECT_EQ(result.get(), (NavMeshDataRef {anotherData, 1})); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_EQ(result.get(), *copy); + EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_used_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; + auto anotherPreparedNavMeshData = makePeparedNavMeshData(3); - const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); + const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, + std::move(anotherPreparedNavMeshData))); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_set_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); + const auto copy = clone(*mPreparedNavMeshData); - const std::vector leastRecentlySetWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh leastRecentlySetRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, leastRecentlySetWater}; - const auto leastRecentlySetData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData leastRecentlySetNavMeshData {leastRecentlySetData, 1}; + const std::vector leastRecentlySetWater {1, Cell {1, osg::Vec3f()}}; + const RecastMesh leastRecentlySetRecastMesh {mGeneration, mRevision, mMesh, leastRecentlySetWater, + mHeightfields, mFlatHeightfields}; + auto leastRecentlySetData = makePeparedNavMeshData(3); - const std::vector mostRecentlySetWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh mostRecentlySetRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, mostRecentlySetWater}; - const auto mostRecentlySetData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mostRecentlySetNavMeshData {mostRecentlySetData, 1}; + const std::vector mostRecentlySetWater {1, Cell {2, osg::Vec3f()}}; + const RecastMesh mostRecentlySetRecastMesh {mGeneration, mRevision, mMesh, mostRecentlySetWater, + mHeightfields, mFlatHeightfields}; + auto mostRecentlySetData = makePeparedNavMeshData(3); - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections, - std::move(leastRecentlySetNavMeshData))); - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections, - std::move(mostRecentlySetNavMeshData))); + ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, + std::move(leastRecentlySetData))); + ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, + std::move(mostRecentlySetData))); - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh)); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_used_value) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector leastRecentlyUsedWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh leastRecentlyUsedRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, leastRecentlyUsedWater}; - const auto leastRecentlyUsedData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData leastRecentlyUsedNavMeshData {leastRecentlyUsedData, 1}; + const std::vector leastRecentlyUsedWater {1, Cell {1, osg::Vec3f()}}; + const RecastMesh leastRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, leastRecentlyUsedWater, + mHeightfields, mFlatHeightfields}; + auto leastRecentlyUsedData = makePeparedNavMeshData(3); + const auto leastRecentlyUsedCopy = clone(*leastRecentlyUsedData); - const std::vector mostRecentlyUsedWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh mostRecentlyUsedRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, mostRecentlyUsedWater}; - const auto mostRecentlyUsedData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData mostRecentlyUsedNavMeshData {mostRecentlyUsedData, 1}; + const std::vector mostRecentlyUsedWater {1, Cell {2, osg::Vec3f()}}; + const RecastMesh mostRecentlyUsedRecastMesh {mGeneration, mRevision, mMesh, mostRecentlyUsedWater, + mHeightfields, mFlatHeightfields}; + auto mostRecentlyUsedData = makePeparedNavMeshData(3); + const auto mostRecentlyUsedCopy = clone(*mostRecentlyUsedData); - cache.set(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections, - std::move(leastRecentlyUsedNavMeshData)); - cache.set(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections, - std::move(mostRecentlyUsedNavMeshData)); + cache.set(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, std::move(leastRecentlyUsedData)); + cache.set(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, std::move(mostRecentlyUsedData)); { - const auto value = cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections); + const auto value = cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh); ASSERT_TRUE(value); - ASSERT_EQ(value.get(), (NavMeshDataRef {leastRecentlyUsedData, 1})); + ASSERT_EQ(value.get(), *leastRecentlyUsedCopy); } { - const auto value = cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections); + const auto value = cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh); ASSERT_TRUE(value); - ASSERT_EQ(value.get(), (NavMeshDataRef {mostRecentlyUsedData, 1})); + ASSERT_EQ(value.get(), *mostRecentlyUsedCopy); } - const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); - EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1})); + const auto copy = clone(*mPreparedNavMeshData); + const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); + EXPECT_EQ(result.get(), *copy); - EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh)); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_cache_max_size) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = 2 * (navMeshDataSize + navMeshKeySize); + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water}; - const auto tooLargeData = reinterpret_cast(dtAlloc(2, DT_ALLOC_PERM)); - NavMeshData tooLargeNavMeshData {tooLargeData, 2}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, water, + mHeightfields, mFlatHeightfields}; + auto tooLargeData = makePeparedNavMeshData(10); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections, - std::move(tooLargeNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_size_of_unused_items) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize1 = cRecastMeshKeySize; - const std::size_t navMeshKeySize2 = cRecastMeshWithWaterKeySize; - const std::size_t maxSize = 2 * navMeshDataSize + navMeshKeySize1 + navMeshKeySize2; + const std::size_t maxSize = 2 * (mRecastMeshWithWaterSize + mPreparedNavMeshDataSize); NavMeshTilesCache cache(maxSize); - const std::vector anotherWater {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, anotherWater}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector anotherWater {1, Cell {1, osg::Vec3f()}}; + const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, anotherWater, + mHeightfields, mFlatHeightfields}; + auto anotherData = makePeparedNavMeshData(3); - const std::vector tooLargeWater {1, RecastMesh::Water {2, btTransform::getIdentity()}}; - const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, tooLargeWater}; - const auto tooLargeData = reinterpret_cast(dtAlloc(2, DT_ALLOC_PERM)); - NavMeshData tooLargeNavMeshData {tooLargeData, 2}; + const std::vector tooLargeWater {1, Cell {2, osg::Vec3f()}}; + const RecastMesh tooLargeRecastMesh {mGeneration, mRevision, mMesh, tooLargeWater, + mHeightfields, mFlatHeightfields}; + auto tooLargeData = makePeparedNavMeshData(10); - const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, - std::move(mNavMeshData)); + const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, + std::move(mPreparedNavMeshData)); ASSERT_TRUE(value); - ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections, - std::move(tooLargeNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections)); + ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, + std::move(anotherData))); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, + std::move(tooLargeData))); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, anotherRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_used_after_set_then_used_by_get_item_should_left_this_item_available) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, - mAreaTypes, water}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; + auto anotherData = makePeparedNavMeshData(3); - const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); + const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); ASSERT_TRUE(firstCopy); { - const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh); ASSERT_TRUE(secondCopy); } - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); } TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_twice_used_item_should_left_this_item_available) { - const std::size_t navMeshDataSize = 1; - const std::size_t navMeshKeySize = cRecastMeshKeySize; - const std::size_t maxSize = navMeshDataSize + navMeshKeySize; + const std::size_t maxSize = mRecastMeshWithWaterSize + mPreparedNavMeshDataSize; NavMeshTilesCache cache(maxSize); - const std::vector water {1, RecastMesh::Water {1, btTransform::getIdentity()}}; - const RecastMesh anotherRecastMesh {mGeneration, mRevision, mIndices, mVertices, mAreaTypes, water}; - const auto anotherData = reinterpret_cast(dtAlloc(1, DT_ALLOC_PERM)); - NavMeshData anotherNavMeshData {anotherData, 1}; + const std::vector water {1, Cell {1, osg::Vec3f()}}; + const RecastMesh anotherRecastMesh {mGeneration, mRevision, mMesh, water, mHeightfields, mFlatHeightfields}; + auto anotherData = makePeparedNavMeshData(3); - cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData)); - const auto firstCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, std::move(mPreparedNavMeshData)); + const auto firstCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh); ASSERT_TRUE(firstCopy); { - const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections); + const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh); ASSERT_TRUE(secondCopy); } - EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections, - std::move(anotherNavMeshData))); - EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections)); + EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, std::move(anotherData))); + EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh)); } } diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp index 2624389b70..73d86bd6ee 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp @@ -1,15 +1,20 @@ #include "operators.hpp" #include -#include #include #include +#include +#include +#include #include #include #include #include #include +#include + +#include #include #include @@ -18,9 +23,38 @@ namespace DetourNavigator { - static inline bool operator ==(const RecastMesh::Water& lhs, const RecastMesh::Water& rhs) + static inline bool operator ==(const Cell& lhs, const Cell& rhs) { - return lhs.mCellSize == rhs.mCellSize && lhs.mTransform == rhs.mTransform; + return lhs.mSize == rhs.mSize && lhs.mShift == rhs.mShift; + } + + static inline bool operator==(const Heightfield& lhs, const Heightfield& rhs) + { + return makeTuple(lhs) == makeTuple(rhs); + } + + static inline bool operator==(const FlatHeightfield& lhs, const FlatHeightfield& rhs) + { + return std::tie(lhs.mBounds, lhs.mHeight) == std::tie(rhs.mBounds, rhs.mHeight); + } + + static inline std::ostream& operator<<(std::ostream& s, const FlatHeightfield& v) + { + return s << "FlatHeightfield {" << v.mBounds << ", " << v.mHeight << "}"; + } + + static inline std::ostream& operator<<(std::ostream& s, const Heightfield& v) + { + s << "Heightfield {.mBounds=" << v.mBounds + << ", .mLength=" << int(v.mLength) + << ", .mMinHeight=" << v.mMinHeight + << ", .mMaxHeight=" << v.mMaxHeight + << ", .mShift=" << v.mShift + << ", .mScale=" << v.mScale + << ", .mHeights={"; + for (float h : v.mHeights) + s << h << ", "; + return s << "}}"; } } @@ -31,14 +65,12 @@ namespace struct DetourNavigatorRecastMeshBuilderTest : Test { - Settings mSettings; TileBounds mBounds; const std::size_t mGeneration = 0; const std::size_t mRevision = 0; DetourNavigatorRecastMeshBuilderTest() { - mSettings.mRecastScaleFactor = 1.0f; mBounds.mMin = osg::Vec2f(-std::numeric_limits::max() * std::numeric_limits::epsilon(), -std::numeric_limits::max() * std::numeric_limits::epsilon()); mBounds.mMax = osg::Vec2f(std::numeric_limits::max() * std::numeric_limits::epsilon(), @@ -48,11 +80,11 @@ namespace TEST_F(DetourNavigatorRecastMeshBuilderTest, create_for_empty_should_return_empty) { - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector()); - EXPECT_EQ(recastMesh->getIndices(), std::vector()); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector()); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector()); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape) @@ -61,16 +93,16 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_bhv_triangle_mesh_shape) @@ -78,70 +110,70 @@ namespace btTriangleMesh mesh; mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 2, 3, 0, - 0, 3, 4, - 0, 3, 0, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 0, 0, 3, + 0, 4, 3, + 2, 0, 3, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_terrian_shape) { const std::array heightfieldData {{0, 0, 0, 0}}; btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -0.5, 0, -0.5, - -0.5, 0, 0.5, - 0.5, 0, -0.5, - 0.5, 0, 0.5, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 2, 1, 3})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -0.5, -0.5, 0, + -0.5, 0.5, 0, + 0.5, -0.5, 0, + 0.5, 0.5, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({0, 1, 2, 2, 1, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_box_shape_should_produce_12_triangles) { btBoxShape shape(btVector3(1, 1, 2)); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 2, 1, - -1, 2, 1, - 1, 2, -1, - -1, 2, -1, - 1, -2, 1, - -1, -2, 1, - 1, -2, -1, - -1, -2, -1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({ - 0, 2, 3, - 3, 1, 0, - 0, 4, 6, - 6, 2, 0, + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, -2, + -1, -1, 2, + -1, 1, -2, + -1, 1, 2, + 1, -1, -2, + 1, -1, 2, + 1, 1, -2, + 1, 1, 2, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ 0, 1, 5, - 5, 4, 0, - 7, 5, 1, + 0, 2, 3, + 0, 4, 6, 1, 3, 7, - 7, 3, 2, 2, 6, 7, - 7, 6, 4, + 3, 1, 0, 4, 5, 7, - })) << recastMesh->getIndices(); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(12, AreaType_ground)); + 5, 4, 0, + 6, 2, 0, + 7, 3, 2, + 7, 5, 1, + 7, 6, 4, + })) << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(12, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_compound_shape) @@ -157,44 +189,44 @@ namespace shape.addChildShape(btTransform::getIdentity(), &triangle1); shape.addChildShape(btTransform::getIdentity(), &box); shape.addChildShape(btTransform::getIdentity(), &triangle2); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -1, -2, -1, - -1, -2, 1, - -1, 0, -1, - -1, 0, 1, - -1, 2, -1, - -1, 2, 1, - 1, -2, -1, - 1, -2, 1, - 1, 0, -1, - 1, 0, 1, - 1, 2, -1, - 1, 2, 1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({ - 8, 3, 2, - 11, 10, 4, - 4, 5, 11, - 11, 7, 6, - 6, 10, 11, - 11, 5, 1, - 1, 7, 11, - 0, 1, 5, - 5, 4, 0, - 0, 4, 10, - 10, 6, 0, - 0, 6, 7, - 7, 1, 0, - 8, 3, 9, - })) << recastMesh->getIndices(); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(14, AreaType_ground)); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, -2, + -1, -1, 0, + -1, -1, 2, + -1, 1, -2, + -1, 1, 0, + -1, 1, 2, + 1, -1, -2, + 1, -1, 0, + 1, -1, 2, + 1, 1, -2, + 1, 1, 0, + 1, 1, 2, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({ + 0, 2, 8, + 0, 3, 5, + 0, 6, 9, + 2, 5, 11, + 3, 9, 11, + 5, 2, 0, + 6, 8, 11, + 7, 4, 1, + 7, 4, 10, + 8, 6, 0, + 9, 3, 0, + 11, 5, 3, + 11, 8, 2, + 11, 9, 6, + })) << recastMesh->getMesh().getIndices(); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(14, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape) @@ -204,20 +236,20 @@ namespace btBvhTriangleMeshShape triangle(&mesh, true); btCompoundShape shape; shape.addChildShape(btTransform::getIdentity(), &triangle); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 2, 3, 0, - 0, 3, 4, - 0, 3, 0, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 0, 0, 3, + 0, 4, 3, + 2, 0, 3, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape_with_transformed_bhv_triangle_shape) @@ -228,20 +260,20 @@ namespace btCompoundShape shape; shape.addChildShape(btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), &triangle); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 3, 12, 2, - 1, 12, 10, - 1, 12, 2, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + 1, 2, 12, + 1, 10, 12, + 3, 2, 12, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, without_bounds_add_bhv_triangle_shape_should_not_filter_by_bounds) @@ -250,48 +282,47 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - -2, 0, -3, - -3, 0, -2, - -3, 0, -3, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 3, 4, 5})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector(2, AreaType_ground)); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 5, 4, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector(2, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_bhv_triangle_shape_should_filter_by_bounds) { - mSettings.mRecastScaleFactor = 0.1f; - mBounds.mMin = osg::Vec2f(-3, -3) * mSettings.mRecastScaleFactor; - mBounds.mMax = osg::Vec2f(-2, -2) * mSettings.mRecastScaleFactor; + mBounds.mMin = osg::Vec2f(-3, -3); + mBounds.mMax = osg::Vec2f(-2, -2); btTriangleMesh mesh; mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform::getIdentity(), AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -0.2f, 0, -0.3f, - -0.3f, 0, -0.2f, - -0.3f, 0, -0.3f, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_x_bhv_triangle_shape_should_filter_by_bounds) @@ -302,7 +333,7 @@ namespace mesh.addTriangle(btVector3(0, -1, -1), btVector3(0, -1, -1), btVector3(0, 1, -1)); mesh.addTriangle(btVector3(0, -3, -3), btVector3(0, -3, -2), btVector3(0, -2, -3)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(1, 0, 0), @@ -310,13 +341,13 @@ namespace AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - 0, -0.70710659027099609375, -3.535533905029296875, - 0, 0.707107067108154296875, -3.535533905029296875, - 0, 2.384185791015625e-07, -4.24264049530029296875, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector({ + 0, -4.24264049530029296875, 4.44089209850062616169452667236328125e-16, + 0, -3.535533905029296875, -0.707106769084930419921875, + 0, -3.535533905029296875, 0.707106769084930419921875, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({1, 2, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_y_bhv_triangle_shape_should_filter_by_bounds) @@ -327,7 +358,7 @@ namespace mesh.addTriangle(btVector3(-1, 0, -1), btVector3(-1, 0, 1), btVector3(1, 0, -1)); mesh.addTriangle(btVector3(-3, 0, -3), btVector3(-3, 0, -2), btVector3(-2, 0, -3)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(0, 1, 0), @@ -335,13 +366,13 @@ namespace AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - -3.535533905029296875, -0.70710659027099609375, 0, - -3.535533905029296875, 0.707107067108154296875, 0, - -4.24264049530029296875, 2.384185791015625e-07, 0, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector({ + -4.24264049530029296875, 0, 4.44089209850062616169452667236328125e-16, + -3.535533905029296875, 0, -0.707106769084930419921875, + -3.535533905029296875, 0, 0.707106769084930419921875, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({1, 2, 0})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_z_bhv_triangle_shape_should_filter_by_bounds) @@ -352,7 +383,7 @@ namespace mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape), btTransform(btQuaternion(btVector3(0, 0, 1), @@ -360,13 +391,13 @@ namespace AreaType_ground ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_THAT(recastMesh->getVertices(), Pointwise(FloatNear(1e-5), std::vector({ - 1.41421353816986083984375, 0, 1.1920928955078125e-07, - -1.41421353816986083984375, 0, -1.1920928955078125e-07, - 1.1920928955078125e-07, 0, -1.41421353816986083984375, - }))); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground})); + EXPECT_THAT(recastMesh->getMesh().getVertices(), Pointwise(FloatNear(1e-5), std::vector({ + -1.41421353816986083984375, -1.1102230246251565404236316680908203125e-16, 0, + 1.1102230246251565404236316680908203125e-16, -1.41421353816986083984375, 0, + 1.41421353816986083984375, 1.1102230246251565404236316680908203125e-16, 0, + }))) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 0, 1})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, flags_values_should_be_corresponding_to_added_objects) @@ -377,7 +408,7 @@ namespace btTriangleMesh mesh2; mesh2.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0)); btBvhTriangleMeshShape shape2(&mesh2, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject( static_cast(shape1), btTransform::getIdentity(), @@ -389,25 +420,25 @@ namespace AreaType_null ); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - 1, 0, -1, - -1, 0, 1, - -1, 0, -1, - -2, 0, -3, - -3, 0, -2, - -3, 0, -3, - })); - EXPECT_EQ(recastMesh->getIndices(), std::vector({0, 1, 2, 3, 4, 5})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_null})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -3, -3, 0, + -3, -2, 0, + -2, -3, 0, + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 5, 4, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_null, AreaType_ground})); } TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it) { - RecastMeshBuilder builder(mSettings, mBounds); - builder.addWater(1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))); + RecastMeshBuilder builder(mBounds); + builder.addWater(1000, osg::Vec3f(100, 200, 300)); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getWater(), std::vector({ - RecastMesh::Water {1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))} + EXPECT_EQ(recastMesh->getWater(), std::vector({ + Cell {1000, osg::Vec3f(100, 200, 300)} })); } @@ -418,16 +449,77 @@ namespace mesh.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0)); btBvhTriangleMeshShape shape(&mesh, true); - RecastMeshBuilder builder(mSettings, mBounds); + RecastMeshBuilder builder(mBounds); builder.addObject(static_cast(shape), btTransform::getIdentity(), AreaType_ground); const auto recastMesh = std::move(builder).create(mGeneration, mRevision); - EXPECT_EQ(recastMesh->getVertices(), std::vector({ - -1, 0, -1, - -1, 0, 1, - 1, 0, -1, - 1, 0, 1, - })) << recastMesh->getVertices(); - EXPECT_EQ(recastMesh->getIndices(), std::vector({2, 1, 0, 2, 1, 3})); - EXPECT_EQ(recastMesh->getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + EXPECT_EQ(recastMesh->getMesh().getVertices(), std::vector({ + -1, -1, 0, + -1, 1, 0, + 1, -1, 0, + 1, 1, 0, + })) << recastMesh->getMesh().getVertices(); + EXPECT_EQ(recastMesh->getMesh().getIndices(), std::vector({2, 1, 0, 2, 1, 3})); + EXPECT_EQ(recastMesh->getMesh().getAreaTypes(), std::vector({AreaType_ground, AreaType_ground})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_flat_heightfield_should_add_intersection) + { + mBounds.mMin = osg::Vec2f(0, 0); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), 10); + 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}, + })); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_inside_tile) + { + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(1000, osg::Vec3f(1, 2, 3), heights.data(), 3, 0, 8); + 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.mHeights = { + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); + } + + TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_should_add_intersection) + { + constexpr std::array heights {{ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, + }}; + mBounds.mMin = osg::Vec2f(250, 250); + RecastMeshBuilder builder(mBounds); + builder.addHeightfield(1000, osg::Vec3f(-1, -2, 3), heights.data(), 3, 0, 8); + const auto recastMesh = std::move(builder).create(mGeneration, mRevision); + Heightfield expected; + expected.mBounds = TileBounds {osg::Vec2f(250, 250), osg::Vec2f(499, 498)}; + 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, + }; + EXPECT_EQ(recastMesh->getHeightfields(), std::vector({expected})); } } diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp index 621db51a89..7751d5220c 100644 --- a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp +++ b/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp @@ -14,20 +14,22 @@ namespace struct DetourNavigatorRecastMeshObjectTest : Test { - btBoxShape mBoxShape {btVector3(1, 2, 3)}; - btCompoundShape mCompoundShape {true}; + btBoxShape mBoxShapeImpl {btVector3(1, 2, 3)}; + CollisionShape mBoxShape {nullptr, mBoxShapeImpl}; + btCompoundShape mCompoundShapeImpl {true}; + CollisionShape mCompoundShape {nullptr, mCompoundShapeImpl}; btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)}; DetourNavigatorRecastMeshObjectTest() { - mCompoundShape.addChildShape(mTransform, std::addressof(mBoxShape)); + mCompoundShapeImpl.addChildShape(mTransform, std::addressof(mBoxShapeImpl)); } }; TEST_F(DetourNavigatorRecastMeshObjectTest, constructed_object_should_have_shape_and_transform) { const RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); - EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShape)); + EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShapeImpl)); EXPECT_EQ(object.getTransform(), mTransform); } @@ -58,14 +60,14 @@ namespace TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_changed_child_transform_should_return_true) { RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); - mCompoundShape.updateChildTransform(0, btTransform::getIdentity()); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); EXPECT_TRUE(object.update(mTransform, AreaType_ground)); } TEST_F(DetourNavigatorRecastMeshObjectTest, repeated_update_for_compound_shape_without_changes_should_return_false) { RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground); - mCompoundShape.updateChildTransform(0, btTransform::getIdentity()); + mCompoundShapeImpl.updateChildTransform(0, btTransform::getIdentity()); object.update(mTransform, AreaType_ground); EXPECT_FALSE(object.update(mTransform, AreaType_ground)); } @@ -73,7 +75,7 @@ namespace TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_changed_local_scaling_should_return_true) { RecastMeshObject object(mBoxShape, mTransform, AreaType_ground); - mBoxShape.setLocalScaling(btVector3(2, 2, 2)); + mBoxShapeImpl.setLocalScaling(btVector3(2, 2, 2)); EXPECT_TRUE(object.update(mTransform, AreaType_ground)); } } diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp index fb0f97831a..6209ec9c2a 100644 --- a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp @@ -38,12 +38,6 @@ namespace EXPECT_EQ(manager.getMesh(TilePosition(0, 0)), nullptr); } - TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, has_tile_for_empty_should_return_false) - { - TileCachedRecastMeshManager manager(mSettings); - EXPECT_FALSE(manager.hasTile(TilePosition(0, 0))); - } - TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero) { const TileCachedRecastMeshManager manager(mSettings); @@ -62,25 +56,28 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - EXPECT_TRUE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(nullptr, boxShape); + EXPECT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_for_existing_object_should_return_false) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); + EXPECT_FALSE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(nullptr, boxShape); + ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground)); for (int x = -1; x < 1; ++x) for (int y = -1; y < 1; ++y) - ASSERT_TRUE(manager.hasTile(TilePosition(x, y))); + ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, update_object_for_changed_object_should_return_changed_tiles) @@ -88,8 +85,9 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); - EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground); + EXPECT_TRUE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [&] (const auto& v) { onChangedTile(v); })); EXPECT_THAT( mChangedTiles, @@ -102,8 +100,9 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); - EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); + EXPECT_FALSE(manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [&] (const auto& v) { onChangedTile(v); })); EXPECT_EQ(mChangedTiles, std::vector()); } @@ -112,7 +111,8 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); @@ -123,7 +123,8 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr); } @@ -132,14 +133,15 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(nullptr, boxShape); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(1, -1)), nullptr); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); @@ -151,12 +153,13 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); + const CollisionShape shape(nullptr, boxShape); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground); EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_EQ(manager.getMesh(TilePosition(1, 0)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(1, -1)), nullptr); } @@ -165,7 +168,8 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); manager.removeObject(ObjectId(&boxShape)); EXPECT_EQ(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_EQ(manager.getMesh(TilePosition(-1, 0)), nullptr); @@ -177,14 +181,15 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); + const CollisionShape shape(nullptr, boxShape); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, 0)), nullptr); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_NE(manager.getMesh(TilePosition(-1, -1)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(-1, 0)), nullptr); EXPECT_NE(manager.getMesh(TilePosition(0, -1)), nullptr); @@ -196,7 +201,8 @@ namespace TileCachedRecastMeshManager manager(mSettings); const auto initialRevision = manager.getRevision(); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); EXPECT_EQ(manager.getRevision(), initialRevision + 1); } @@ -204,9 +210,10 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); const auto beforeAddRevision = manager.getRevision(); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); EXPECT_EQ(manager.getRevision(), beforeAddRevision); } @@ -215,9 +222,10 @@ namespace TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); const btTransform transform(btMatrix3x3::getIdentity(), btVector3(getTileSize(mSettings) / mSettings.mRecastScaleFactor, 0, 0)); - manager.addObject(ObjectId(&boxShape), boxShape, transform, AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground); const auto beforeUpdateRevision = manager.getRevision(); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision + 1); } @@ -225,9 +233,10 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); const auto beforeUpdateRevision = manager.getRevision(); - manager.updateObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); + manager.updateObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground, [] (auto) {}); EXPECT_EQ(manager.getRevision(), beforeUpdateRevision); } @@ -235,7 +244,8 @@ namespace { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground); + const CollisionShape shape(nullptr, boxShape); + manager.addObject(ObjectId(&boxShape), shape, btTransform::getIdentity(), AreaType::AreaType_ground); const auto beforeRemoveRevision = manager.getRevision(); manager.removeObject(ObjectId(&boxShape)); EXPECT_EQ(manager.getRevision(), beforeRemoveRevision + 1); @@ -254,7 +264,7 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - EXPECT_TRUE(manager.addWater(cellPosition, cellSize, btTransform::getIdentity())); + EXPECT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles) @@ -262,23 +272,24 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, btTransform::getIdentity())); + 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.hasTile(TilePosition(x, y))); + ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_max_int_should_not_add_new_tiles) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(nullptr, boxShape); + 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, btTransform::getIdentity())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_EQ(manager.hasTile(TilePosition(x, y)), -1 <= x && x <= 0 && -1 <= y && y <= 0); + ASSERT_EQ(manager.getMesh(TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_absent_cell_should_return_nullopt) @@ -292,10 +303,10 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, btTransform::getIdentity())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); const auto result = manager.removeWater(cellPosition); ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result->mCellSize, cellSize); + EXPECT_EQ(result->mSize, cellSize); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles) @@ -303,24 +314,40 @@ namespace TileCachedRecastMeshManager manager(mSettings); const osg::Vec2i cellPosition(0, 0); const int cellSize = 8192; - ASSERT_TRUE(manager.addWater(cellPosition, cellSize, btTransform::getIdentity())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); ASSERT_TRUE(manager.removeWater(cellPosition)); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_FALSE(manager.hasTile(TilePosition(x, y))); + ASSERT_EQ(manager.getMesh(TilePosition(x, y)), nullptr); } TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_leave_not_empty_tiles) { TileCachedRecastMeshManager manager(mSettings); const btBoxShape boxShape(btVector3(20, 20, 100)); - ASSERT_TRUE(manager.addObject(ObjectId(&boxShape), boxShape, btTransform::getIdentity(), AreaType::AreaType_ground)); + const CollisionShape shape(nullptr, boxShape); + 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, btTransform::getIdentity())); + ASSERT_TRUE(manager.addWater(cellPosition, cellSize, osg::Vec3f())); ASSERT_TRUE(manager.removeWater(cellPosition)); for (int x = -6; x < 6; ++x) for (int y = -6; y < 6; ++y) - ASSERT_EQ(manager.hasTile(TilePosition(x, y)), -1 <= x && x <= 0 && -1 <= y && y <= 0); + ASSERT_EQ(manager.getMesh(TilePosition(x, y)) != nullptr, -1 <= x && x <= 0 && -1 <= y && y <= 0); + } + + TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_object_should_not_remove_tile_with_water) + { + TileCachedRecastMeshManager manager(mSettings); + const osg::Vec2i cellPosition(0, 0); + const int cellSize = 8192; + 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.removeObject(ObjectId(&boxShape))); + for (int x = -6; x < 6; ++x) + for (int y = -6; y < 6; ++y) + ASSERT_NE(manager.getMesh(TilePosition(x, y)), nullptr); } } diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index 69a326060c..32d4ea49b8 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -77,6 +77,14 @@ return { EXPECT_EQ(LuaUtil::call(script1["get"]).get(), 45); } + TEST_F(LuaStateTest, ToString) + { + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), 3.14)), "3.14"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), true)), "true"); + EXPECT_EQ(LuaUtil::toString(sol::nil), "nil"); + EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), "something")), "\"something\""); + } + TEST_F(LuaStateTest, ErrorHandling) { EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:"); @@ -111,7 +119,7 @@ return { EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); - EXPECT_EQ(mLua.getMutableFromReadOnly(mLua.makeReadOnly(script)), script); + EXPECT_EQ(LuaUtil::getMutableFromReadOnly(LuaUtil::makeReadOnly(script)), script); } TEST_F(LuaStateTest, Print) @@ -142,8 +150,8 @@ return { { LuaUtil::LuaState lua(mVFS.get()); - sol::table api1 = lua.makeReadOnly(lua.sol().create_table_with("name", "api1")); - sol::table api2 = lua.makeReadOnly(lua.sol().create_table_with("name", "api2")); + sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1")); + sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2")); sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api1}}); diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/openmw_test_suite/lua/test_serialization.cpp index d3c01f6298..1983daa158 100644 --- a/apps/openmw_test_suite/lua/test_serialization.cpp +++ b/apps/openmw_test_suite/lua/test_serialization.cpp @@ -163,7 +163,8 @@ namespace { if (sizeof(TestStruct1) != binaryData.size()) throw std::runtime_error("Incorrect binaryData.size() for TestStruct1: " + std::to_string(binaryData.size())); - TestStruct1 t = *reinterpret_cast(binaryData.data()); + TestStruct1 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); t.a = Misc::fromLittleEndian(t.a); t.b = Misc::fromLittleEndian(t.b); sol::stack::push(lua, t); @@ -173,7 +174,8 @@ namespace { if (sizeof(TestStruct2) != binaryData.size()) throw std::runtime_error("Incorrect binaryData.size() for TestStruct2: " + std::to_string(binaryData.size())); - TestStruct2 t = *reinterpret_cast(binaryData.data()); + TestStruct2 t; + std::memcpy(&t, binaryData.data(), sizeof(t)); t.a = Misc::fromLittleEndian(t.a); t.b = Misc::fromLittleEndian(t.b); sol::stack::push(lua, t); @@ -191,9 +193,9 @@ namespace table["y"] = TestStruct2{4, 3}; TestSerializer serializer; - EXPECT_ERROR(LuaUtil::serialize(table), "Unknown userdata"); + EXPECT_ERROR(LuaUtil::serialize(table), "Value is not serializable."); std::string serialized = LuaUtil::serialize(table, &serializer); - EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type:"); + EXPECT_ERROR(LuaUtil::deserialize(lua, serialized), "Unknown type in serialized data:"); sol::table res = LuaUtil::deserialize(lua, serialized, &serializer); TestStruct1 rx = res.get("x"); diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/openmw_test_suite/lua/test_utilpackage.cpp index afd9fa2d3c..fb8e48e461 100644 --- a/apps/openmw_test_suite/lua/test_utilpackage.cpp +++ b/apps/openmw_test_suite/lua/test_utilpackage.cpp @@ -1,6 +1,7 @@ #include "gmock/gmock.h" #include +#include #include #include "testing_util.hpp" @@ -45,6 +46,7 @@ namespace EXPECT_FLOAT_EQ(lua.safe_script("return v.y").get(), 12); EXPECT_FLOAT_EQ(lua.safe_script("return v.z").get(), 13); EXPECT_EQ(lua.safe_script("return tostring(v)").get(), "(5, 12, 13)"); + EXPECT_EQ(LuaUtil::toString(lua.safe_script("return v")), "(5, 12, 13)"); EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length()").get(), 5); EXPECT_FLOAT_EQ(lua.safe_script("return util.vector3(4, 0, 3):length2()").get(), 25); EXPECT_FALSE(lua.safe_script("return util.vector3(1, 2, 3) == util.vector3(1, 3, 2)").get()); diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/openmw_test_suite/misc/test_stringops.cpp index 086908692d..173cfa4447 100644 --- a/apps/openmw_test_suite/misc/test_stringops.cpp +++ b/apps/openmw_test_suite/misc/test_stringops.cpp @@ -1,6 +1,10 @@ #include #include "components/misc/stringops.hpp" +#include +#include +#include + struct PartialBinarySearchTest : public ::testing::Test { protected: @@ -12,10 +16,6 @@ struct PartialBinarySearchTest : public ::testing::Test std::sort(mDataVec.begin(), mDataVec.end(), Misc::StringUtils::ciLess); } - void TearDown() override - { - } - bool matches(const std::string& keyword) { return Misc::StringUtils::partialBinarySearch(mDataVec.begin(), mDataVec.end(), keyword) != mDataVec.end(); @@ -51,3 +51,97 @@ TEST_F (PartialBinarySearchTest, ci_test) std::string unicode1 = "\u04151 \u0418"; // CYRILLIC CAPITAL LETTER IE, CYRILLIC CAPITAL LETTER I EXPECT_TRUE( Misc::StringUtils::lowerCase(unicode1) == unicode1 ); } + +namespace +{ + using ::Misc::StringUtils; + using namespace ::testing; + + template + struct MiscStringUtilsCiEqualEmptyTest : Test {}; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest); + + TYPED_TEST_P(MiscStringUtilsCiEqualEmptyTest, empty_strings_should_be_equal) + { + EXPECT_TRUE(StringUtils::ciEqual(typename TypeParam::first_type {}, typename TypeParam::second_type {})); + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualEmptyTest, + empty_strings_should_be_equal + ); + + using EmptyStringTypePairsTypes = Types< + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair + >; + + INSTANTIATE_TYPED_TEST_SUITE_P(EmptyStringTypePairs, MiscStringUtilsCiEqualEmptyTest, EmptyStringTypePairsTypes); + + template + struct MiscStringUtilsCiEqualNotEmptyTest : Test {}; + + TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest); + + using RawValue = const char[4]; + + constexpr RawValue foo = "foo"; + constexpr RawValue fooUpper = "FOO"; + constexpr RawValue bar = "bar"; + + template + using Value = std::conditional_t, RawValue&, T>; + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_should_be_equal) + { + const Value a {foo}; + const Value b {foo}; + EXPECT_TRUE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, same_strings_with_different_case_sensetivity_should_be_equal) + { + const Value a {foo}; + const Value b {fooUpper}; + EXPECT_TRUE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + TYPED_TEST_P(MiscStringUtilsCiEqualNotEmptyTest, different_strings_content_should_not_be_equal) + { + const Value a {foo}; + const Value b {bar}; + EXPECT_FALSE(StringUtils::ciEqual(a, b)) << a << "\n" << b; + } + + REGISTER_TYPED_TEST_SUITE_P(MiscStringUtilsCiEqualNotEmptyTest, + same_strings_should_be_equal, + same_strings_with_different_case_sensetivity_should_be_equal, + different_strings_content_should_not_be_equal + ); + + using NotEmptyStringTypePairsTypes = Types< + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair, + std::pair + >; + + INSTANTIATE_TYPED_TEST_SUITE_P(NotEmptyStringTypePairs, MiscStringUtilsCiEqualNotEmptyTest, NotEmptyStringTypePairsTypes); + + TEST(MiscStringUtilsCiEqualTest, string_with_different_length_should_not_be_equal) + { + EXPECT_FALSE(StringUtils::ciEqual(std::string("a"), std::string("aa"))); + } +} diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp index 431725be2c..62b6f67aae 100644 --- a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp +++ b/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp @@ -27,9 +27,9 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution) search.highlightKeywords(text.begin(), text.end(), matches); // Should contain: "foo bar", "lock switch" - ASSERT_TRUE (matches.size() == 2); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "foo bar"); - ASSERT_TRUE (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) == "lock switch"); + EXPECT_EQ (matches.size() , 2); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "foo bar"); + EXPECT_EQ (std::string(matches.rbegin()->mBeg, matches.rbegin()->mEnd) , "lock switch"); } TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) @@ -43,8 +43,8 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "dwemer language"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "dwemer language"); } @@ -62,6 +62,27 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); - ASSERT_TRUE (matches.size() == 1); - ASSERT_TRUE (std::string(matches.front().mBeg, matches.front().mEnd) == "bar lock"); + EXPECT_EQ (matches.size() , 1); + EXPECT_EQ (std::string(matches.front().mBeg, matches.front().mEnd) , "bar lock"); +} + + +TEST_F(KeywordSearchTest, keyword_test_utf8_word_begin) +{ + // We make sure that the search works well even if the character is not ASCII + MWDialogue::KeywordSearch search; + search.seed("états", 0); + search.seed("ïrradiés", 0); + search.seed("ça nous déçois", 0); + + + std::string text = "les nations unis ont réunis le monde entier, états units inclus pour parler du problème des gens ïrradiés et ça nous déçois"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ (matches.size() , 3); + EXPECT_EQ (std::string( matches[0].mBeg, matches[0].mEnd) , "états"); + EXPECT_EQ (std::string( matches[1].mBeg, matches[1].mEnd) , "ïrradiés"); + EXPECT_EQ (std::string( matches[2].mBeg, matches[2].mEnd) , "ça nous déçois"); } diff --git a/apps/wizard/CMakeLists.txt b/apps/wizard/CMakeLists.txt index 10e06d1ff0..03cd63480a 100644 --- a/apps/wizard/CMakeLists.txt +++ b/apps/wizard/CMakeLists.txt @@ -1,4 +1,3 @@ - set(WIZARD componentselectionpage.cpp conclusionpage.cpp @@ -34,21 +33,6 @@ set(WIZARD_HEADER utils/componentlistwidget.hpp ) -# Headers that must be pre-processed -set(WIZARD_HEADER_MOC - componentselectionpage.hpp - conclusionpage.hpp - existinginstallationpage.hpp - importpage.hpp - installationtargetpage.hpp - intropage.hpp - languageselectionpage.hpp - mainwizard.hpp - methodselectionpage.hpp - - utils/componentlistwidget.hpp -) - set(WIZARD_UI ${CMAKE_SOURCE_DIR}/files/ui/wizard/componentselectionpage.ui ${CMAKE_SOURCE_DIR}/files/ui/wizard/conclusionpage.ui @@ -63,7 +47,6 @@ set(WIZARD_UI if (OPENMW_USE_UNSHIELD) set (WIZARD ${WIZARD} installationpage.cpp unshield/unshieldworker.cpp) set (WIZARD_HEADER ${WIZARD_HEADER} installationpage.hpp unshield/unshieldworker.hpp) - set (WIZARD_HEADER_MOC ${WIZARD_HEADER_MOC} installationpage.hpp unshield/unshieldworker.hpp) set (WIZARD_UI ${WIZARD_UI} ${CMAKE_SOURCE_DIR}/files/ui/wizard/installationpage.ui) add_definitions(-DOPENMW_USE_UNSHIELD) endif (OPENMW_USE_UNSHIELD) @@ -80,7 +63,6 @@ if(WIN32) endif(WIN32) QT5_ADD_RESOURCES(RCC_SRCS ${CMAKE_SOURCE_DIR}/files/wizard/wizard.qrc) -QT5_WRAP_CPP(MOC_SRCS ${WIZARD_HEADER_MOC}) QT5_WRAP_UI(UI_HDRS ${WIZARD_UI}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -94,12 +76,11 @@ openmw_add_executable(openmw-wizard ${WIZARD} ${WIZARD_HEADER} ${RCC_SRCS} - ${MOC_SRCS} ${UI_HDRS} ) target_link_libraries(openmw-wizard - components + components_qt ) target_link_libraries(openmw-wizard Qt5::Widgets Qt5::Core) @@ -125,3 +106,7 @@ endif() if (WIN32) INSTALL(TARGETS openmw-wizard RUNTIME DESTINATION ".") endif(WIN32) + +if(USE_QT) + set_property(TARGET openmw-wizard PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index 1621a08cfd..0adc185b38 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -78,12 +78,8 @@ foreach (u ${ARGN}) file (GLOB ALL "${dir}/${u}.[ch]pp") foreach (f ${ALL}) list (APPEND files "${f}") -list (APPEND COMPONENT_FILES "${f}") +list (APPEND COMPONENT_QT_FILES "${f}") endforeach (f) -file (GLOB MOC_H "${dir}/${u}.hpp") -foreach (fi ${MOC_H}) -list (APPEND COMPONENT_MOC_FILES "${fi}") -endforeach (fi) endforeach (u) source_group ("components\\${dir}" FILES ${files}) endmacro (add_component_qt_dir) @@ -97,44 +93,21 @@ add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") endmacro (add_unit) -macro (add_qt_unit project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -add_file (${project} _SRC ${comp} "${dir}/${unit}.cpp") -endmacro (add_qt_unit) - macro (add_hdr project dir unit) add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") endmacro (add_hdr) -macro (add_qt_hdr project dir unit) -add_file (${project} _HDR ${comp} "${dir}/${unit}.hpp") -add_file (${project} _HDR_QT ${comp} "${dir}/${unit}.hpp") -endmacro (add_qt_hdr) - macro (opencs_units dir) foreach (u ${ARGN}) -add_qt_unit (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_units) - -macro (opencs_units_noqt dir) -foreach (u ${ARGN}) add_unit (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_units_noqt) +endmacro (opencs_units) macro (opencs_hdrs dir) foreach (u ${ARGN}) -add_qt_hdr (OPENCS ${dir} ${u}) -endforeach (u) -endmacro (opencs_hdrs) - -macro (opencs_hdrs_noqt dir) -foreach (u ${ARGN}) add_hdr (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_hdrs_noqt) +endmacro (opencs_hdrs) include(CMakeParseArguments) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 9d72320ca9..7ee3d184d8 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -56,7 +56,7 @@ 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 - actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller + actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt screencapture ) @@ -193,6 +193,12 @@ add_component_dir(detournavigator navmeshtileview oscillatingrecastmeshobject offmeshconnectionsmanager + preparednavmeshdata + navmeshcacheitem + ) + +add_component_dir(loadinglistener + reporter ) set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui @@ -215,12 +221,11 @@ if (USE_QT) processinvoker ) - add_component_dir (misc + add_component_qt_dir (misc helpviewer ) QT5_WRAP_UI(ESM_UI_HDR ${ESM_UI}) - QT5_WRAP_CPP(MOC_SRCS ${COMPONENT_MOC_FILES}) endif() if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -231,7 +236,7 @@ endif () include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) -add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) +add_library(components STATIC ${COMPONENT_FILES}) target_link_libraries(components # CMake's built-in OSG finder does not use pkgconfig, so we have to @@ -274,13 +279,24 @@ if (WIN32) endif() if (USE_QT) - target_link_libraries(components Qt5::Widgets Qt5::Core) + add_library(components_qt STATIC ${COMPONENT_QT_FILES} ${ESM_UI_HDR}) + target_link_libraries(components_qt components Qt5::Widgets Qt5::Core) + target_compile_definitions(components_qt PRIVATE OPENMW_DOC_BASEURL="${OPENMW_DOC_BASEURL}") endif() if (GIT_CHECKOUT) add_dependencies (components git-version) endif (GIT_CHECKOUT) +if (OSG_STATIC AND CMAKE_SYSTEM_NAME MATCHES "Linux") + find_package(X11 REQUIRED COMPONENTS Xinerama Xrandr) + target_link_libraries(components ${CMAKE_DL_LIBS} X11::X11 X11::Xinerama X11::Xrandr) + find_package(Fontconfig MODULE) + if(Fontconfig_FOUND) + target_link_libraries(components Fontconfig::Fontconfig) + endif() +endif() + if (WIN32) target_link_libraries(components shlwapi) endif() @@ -300,3 +316,35 @@ endif() set(COMPONENT_FILES ${COMPONENT_FILES} PARENT_SCOPE) target_compile_definitions(components PUBLIC BT_USE_DOUBLE_PRECISION) + +if(OSG_STATIC) + unset(_osg_plugins_static_files) + add_library(components_osg_plugins INTERFACE) + foreach(_plugin ${USED_OSG_PLUGINS}) + string(TOUPPER ${_plugin} _plugin_uc) + if(OPENMW_USE_SYSTEM_OSG) + list(APPEND _osg_plugins_static_files ${${_plugin_uc}_LIBRARY}) + else() + list(APPEND _osg_plugins_static_files $) + target_link_libraries(components_osg_plugins INTERFACE $) + add_dependencies(components_osg_plugins ${${_plugin_uc}_LIBRARY}) + endif() + endforeach() + # We use --whole-archive because OSG plugins use registration. + get_whole_archive_options(_opts ${_osg_plugins_static_files}) + target_link_options(components_osg_plugins INTERFACE ${_opts}) + target_link_libraries(components components_osg_plugins) + + if(OPENMW_USE_SYSTEM_OSG) + # OSG plugin pkgconfig files are missing these dependencies. + # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 + find_package(Freetype REQUIRED) + find_package(JPEG REQUIRED) + find_package(PNG REQUIRED) + target_link_libraries(components Freetype::Freetype JPEG::JPEG PNG::PNG) + endif() +endif(OSG_STATIC) + +if(USE_QT) + set_property(TARGET components_qt PROPERTY AUTOMOC ON) +endif(USE_QT) diff --git a/components/compiler/exprparser.cpp b/components/compiler/exprparser.cpp index fb7ca7aac4..23f15c8bf5 100644 --- a/components/compiler/exprparser.cpp +++ b/components/compiler/exprparser.cpp @@ -421,42 +421,26 @@ namespace Compiler if (mNextOperand) { - if (keyword==Scanner::K_getsquareroot) + // check for custom extensions + if (const Extensions *extensions = getContext().getExtensions()) { start(); - mTokenLoc = loc; - parseArguments ("f", scanner); + char returnType; + std::string argumentType; - Generator::squareRoot (mCode); - mOperands.push_back ('f'); + bool hasExplicit = false; - mNextOperand = false; - return true; - } - else - { - // check for custom extensions - if (const Extensions *extensions = getContext().getExtensions()) + if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) { - start(); + mTokenLoc = loc; + int optionals = parseArguments (argumentType, scanner); - char returnType; - std::string argumentType; + extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); + mOperands.push_back (returnType); - bool hasExplicit = false; - - if (extensions->isFunction (keyword, returnType, argumentType, hasExplicit)) - { - mTokenLoc = loc; - int optionals = parseArguments (argumentType, scanner); - - extensions->generateFunctionCode (keyword, mCode, mLiterals, "", optionals); - mOperands.push_back (returnType); - - mNextOperand = false; - return true; - } + mNextOperand = false; + return true; } } } diff --git a/components/compiler/generator.cpp b/components/compiler/generator.cpp index 34da1c4120..3d8b87e69e 100644 --- a/components/compiler/generator.cpp +++ b/components/compiler/generator.cpp @@ -104,11 +104,6 @@ namespace code.push_back (Compiler::Generator::segment5 (17)); } - void opSquareRoot (Compiler::Generator::CodeContainer& code) - { - code.push_back (Compiler::Generator::segment5 (19)); - } - void opReturn (Compiler::Generator::CodeContainer& code) { code.push_back (Compiler::Generator::segment5 (20)); @@ -452,11 +447,6 @@ namespace Compiler::Generator } } - void squareRoot (CodeContainer& code) - { - opSquareRoot (code); - } - void exit (CodeContainer& code) { opReturn (code); diff --git a/components/compiler/generator.hpp b/components/compiler/generator.hpp index 55bba2a75c..00c6e56825 100644 --- a/components/compiler/generator.hpp +++ b/components/compiler/generator.hpp @@ -74,8 +74,6 @@ namespace Compiler void convert (CodeContainer& code, char fromType, char toType); - void squareRoot (CodeContainer& code); - void exit (CodeContainer& code); void message (CodeContainer& code, Literals& literals, const std::string& message, diff --git a/components/compiler/scanner.cpp b/components/compiler/scanner.cpp index 829df35be5..1054e2e269 100644 --- a/components/compiler/scanner.cpp +++ b/components/compiler/scanner.cpp @@ -164,8 +164,6 @@ namespace Compiler std::string value; c.appendTo(value); - bool error = false; - while (get (c)) { if (c.isDigit()) @@ -174,16 +172,11 @@ namespace Compiler } else if (!c.isMinusSign() && isStringCharacter (c)) { - error = true; - c.appendTo(value); + /// workaround that allows names to begin with digits + return scanName(c, parser, cont, value); } else if (c=='.') { - if (error) - { - putback (c); - break; - } return scanFloat (value, parser, cont); } else @@ -193,17 +186,6 @@ namespace Compiler } } - if (error) - { - /// workaround that allows names to begin with digits - /// \todo disable - TokenLoc loc (mLoc); - mLoc.mLiteral.clear(); - cont = parser.parseName (value, loc, *this); - return true; -// return false; - } - TokenLoc loc (mLoc); mLoc.mLiteral.clear(); @@ -265,13 +247,11 @@ namespace Compiler "return", "messagebox", "set", "to", - "getsquareroot", nullptr }; - bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont) + bool Scanner::scanName (MultiChar& c, Parser& parser, bool& cont, std::string name) { - std::string name; c.appendTo(name); if (!scanName (name)) diff --git a/components/compiler/scanner.hpp b/components/compiler/scanner.hpp index 15781f9240..8ee2672132 100644 --- a/components/compiler/scanner.hpp +++ b/components/compiler/scanner.hpp @@ -205,8 +205,7 @@ namespace Compiler K_while, K_endwhile, K_return, K_messagebox, - K_set, K_to, - K_getsquareroot + K_set, K_to }; enum special @@ -237,7 +236,7 @@ namespace Compiler bool scanFloat (const std::string& intValue, Parser& parser, bool& cont); - bool scanName (MultiChar& c, Parser& parser, bool& cont); + bool scanName (MultiChar& c, Parser& parser, bool& cont, std::string name = {}); /// \param name May contain the start of the name (one or more characters) bool scanName (std::string& name); diff --git a/components/compiler/stringparser.cpp b/components/compiler/stringparser.cpp index 8b20377e1f..d9c3c04947 100644 --- a/components/compiler/stringparser.cpp +++ b/components/compiler/stringparser.cpp @@ -63,7 +63,7 @@ namespace Compiler keyword==Scanner::K_elseif || keyword==Scanner::K_while || keyword==Scanner::K_endwhile || keyword==Scanner::K_return || keyword==Scanner::K_messagebox || keyword==Scanner::K_set || - keyword==Scanner::K_to || keyword==Scanner::K_getsquareroot) + keyword==Scanner::K_to) { // pretend this is not a keyword std::string name = loc.mLiteral; diff --git a/components/detournavigator/asyncnavmeshupdater.cpp b/components/detournavigator/asyncnavmeshupdater.cpp index 11de607456..5c88d0cfa2 100644 --- a/components/detournavigator/asyncnavmeshupdater.cpp +++ b/components/detournavigator/asyncnavmeshupdater.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include #include @@ -18,6 +20,10 @@ namespace { using DetourNavigator::ChangeType; using DetourNavigator::TilePosition; + using DetourNavigator::UpdateType; + using DetourNavigator::ChangeType; + using DetourNavigator::Job; + using DetourNavigator::JobIt; int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs) { @@ -25,44 +31,61 @@ namespace } int getMinDistanceTo(const TilePosition& position, int maxDistance, - const std::map>& tilesPerHalfExtents, + const std::set>& pushedTiles, const std::set>& presentTiles) { int result = maxDistance; - for (const auto& [halfExtents, tiles] : tilesPerHalfExtents) - for (const TilePosition& tile : tiles) - if (presentTiles.find(std::make_tuple(halfExtents, tile)) == presentTiles.end()) - result = std::min(result, getManhattanDistance(position, tile)); + for (const auto& [halfExtents, tile] : pushedTiles) + if (presentTiles.find(std::tie(halfExtents, tile)) == presentTiles.end()) + result = std::min(result, getManhattanDistance(position, tile)); return result; } + + UpdateType getUpdateType(ChangeType changeType) noexcept + { + if (changeType == ChangeType::update) + return UpdateType::Temporary; + return UpdateType::Persistent; + } + + auto getPriority(const Job& job) noexcept + { + return std::make_tuple(job.mProcessTime, job.mChangeType, job.mTryNumber, job.mDistanceToPlayer, job.mDistanceToOrigin); + } + + struct LessByJobPriority + { + bool operator()(JobIt lhs, JobIt rhs) const noexcept + { + return getPriority(*lhs) < getPriority(*rhs); + } + }; + + void insertPrioritizedJob(JobIt job, std::deque& queue) + { + const auto it = std::upper_bound(queue.begin(), queue.end(), job, LessByJobPriority {}); + queue.insert(it, job); + } + + auto getAgentAndTile(const Job& job) noexcept + { + return std::make_tuple(job.mAgentHalfExtents, job.mChangedTile); + } } namespace DetourNavigator { - static std::ostream& operator <<(std::ostream& stream, UpdateNavMeshStatus value) + Job::Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr navMeshCacheItem, + const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, + std::chrono::steady_clock::time_point processTime) + : mAgentHalfExtents(agentHalfExtents) + , mNavMeshCacheItem(std::move(navMeshCacheItem)) + , mChangedTile(changedTile) + , mProcessTime(processTime) + , mChangeType(changeType) + , mDistanceToPlayer(distanceToPlayer) + , mDistanceToOrigin(getManhattanDistance(changedTile, TilePosition {0, 0})) { - switch (value) - { - case UpdateNavMeshStatus::ignored: - return stream << "ignore"; - case UpdateNavMeshStatus::removed: - return stream << "removed"; - case UpdateNavMeshStatus::added: - return stream << "add"; - case UpdateNavMeshStatus::replaced: - return stream << "replaced"; - case UpdateNavMeshStatus::failed: - return stream << "failed"; - case UpdateNavMeshStatus::lost: - return stream << "lost"; - case UpdateNavMeshStatus::cached: - return stream << "cached"; - case UpdateNavMeshStatus::unchanged: - return stream << "unchanged"; - case UpdateNavMeshStatus::restored: - return stream << "restored"; - } - return stream << "unknown(" << static_cast(value) << ")"; } AsyncNavMeshUpdater::AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager, @@ -81,7 +104,7 @@ namespace DetourNavigator { mShouldStop = true; std::unique_lock lock(mMutex); - mJobs = decltype(mJobs)(); + mWaiting.clear(); mHasJob.notify_all(); lock.unlock(); for (auto& thread : mThreads) @@ -102,47 +125,44 @@ namespace DetourNavigator if (!playerTileChanged && changedTiles.empty()) return; + const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + const std::lock_guard lock(mMutex); if (playerTileChanged) - for (auto& job : mJobs) - job.mDistanceToPlayer = getManhattanDistance(job.mChangedTile, playerTile); - - for (const auto& changedTile : changedTiles) { - if (mPushed[agentHalfExtents].insert(changedTile.first).second) + for (JobIt job : mWaiting) { - Job job; + job->mDistanceToPlayer = getManhattanDistance(job->mChangedTile, playerTile); + if (!shouldAddTile(job->mChangedTile, playerTile, std::min(mSettings.get().mMaxTilesNumber, params.maxTiles))) + job->mChangeType = ChangeType::remove; + } + } - job.mAgentHalfExtents = agentHalfExtents; - job.mNavMeshCacheItem = navMeshCacheItem; - job.mChangedTile = changedTile.first; - job.mTryNumber = 0; - job.mChangeType = changedTile.second; - job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile); - job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0}); - job.mProcessTime = job.mChangeType == ChangeType::update - ? mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] + mSettings.get().mMinUpdateInterval + for (const auto& [changedTile, changeType] : changedTiles) + { + if (mPushed.emplace(agentHalfExtents, changedTile).second) + { + const auto processTime = changeType == ChangeType::update + ? mLastUpdates[std::tie(agentHalfExtents, changedTile)] + mSettings.get().mMinUpdateInterval : std::chrono::steady_clock::time_point(); + const JobIt it = mJobs.emplace(mJobs.end(), agentHalfExtents, navMeshCacheItem, changedTile, + changeType, getManhattanDistance(changedTile, playerTile), processTime); + if (playerTileChanged) - { - mJobs.push_back(std::move(job)); - } + mWaiting.push_back(it); else - { - const auto it = std::upper_bound(mJobs.begin(), mJobs.end(), job); - mJobs.insert(it, std::move(job)); - } + insertPrioritizedJob(it, mWaiting); } } if (playerTileChanged) - std::sort(mJobs.begin(), mJobs.end()); + std::sort(mWaiting.begin(), mWaiting.end(), LessByJobPriority {}); Log(Debug::Debug) << "Posted " << mJobs.size() << " navigator jobs"; - if (!mJobs.empty()) + if (!mWaiting.empty()) mHasJob.notify_all(); } @@ -183,15 +203,13 @@ namespace DetourNavigator int minDistanceToPlayer = 0; const auto isDone = [&] { - jobsLeft = mJobs.size() + getTotalThreadJobsUnsafe(); + jobsLeft = mJobs.size(); if (jobsLeft == 0) { minDistanceToPlayer = 0; return true; } minDistanceToPlayer = getMinDistanceTo(playerPosition, maxDistanceToPlayer, mPushed, mPresentTiles); - for (const auto& [threadId, queue] : mThreadsQueues) - minDistanceToPlayer = getMinDistanceTo(playerPosition, minDistanceToPlayer, queue.mPushed, mPresentTiles); return minDistanceToPlayer >= maxDistanceToPlayer; }; std::unique_lock lock(mMutex); @@ -218,7 +236,7 @@ namespace DetourNavigator { { std::unique_lock lock(mMutex); - mDone.wait(lock, [this] { return mJobs.size() + getTotalThreadJobsUnsafe() == 0; }); + mDone.wait(lock, [this] { return mJobs.size() == 0; }); } mProcessingTiles.wait(mProcessed, [] (const auto& v) { return v.empty(); }); } @@ -226,13 +244,20 @@ namespace DetourNavigator void AsyncNavMeshUpdater::reportStats(unsigned int frameNumber, osg::Stats& stats) const { std::size_t jobs = 0; + std::size_t waiting = 0; + std::size_t pushed = 0; { const std::lock_guard lock(mMutex); - jobs = mJobs.size() + getTotalThreadJobsUnsafe(); + jobs = mJobs.size(); + waiting = mWaiting.size(); + pushed = mPushed.size(); } - stats.setAttribute(frameNumber, "NavMesh UpdateJobs", jobs); + stats.setAttribute(frameNumber, "NavMesh Jobs", jobs); + stats.setAttribute(frameNumber, "NavMesh Waiting", waiting); + stats.setAttribute(frameNumber, "NavMesh Pushed", pushed); + stats.setAttribute(frameNumber, "NavMesh Processing", mProcessingTiles.lockConst()->size()); mNavMeshTilesCache.reportStats(frameNumber, stats); } @@ -245,12 +270,14 @@ namespace DetourNavigator { try { - if (auto job = getNextJob()) + if (JobIt job = getNextJob(); job != mJobs.end()) { const auto processed = processJob(*job); unlockTile(job->mAgentHalfExtents, job->mChangedTile); - if (!processed) - repost(std::move(*job)); + if (processed) + removeJob(job); + else + repost(job); } else cleanupLastUpdates(); @@ -270,8 +297,6 @@ namespace DetourNavigator const auto start = std::chrono::steady_clock::now(); - const auto firstStart = setFirstStart(start); - const auto navMeshCacheItem = job.mNavMeshCacheItem.lock(); if (!navMeshCacheItem) @@ -282,7 +307,7 @@ namespace DetourNavigator const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile); const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile, - offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache); + offMeshConnections, mSettings, navMeshCacheItem, mNavMeshTilesCache, getUpdateType(job.mChangeType)); if (recastMesh != nullptr) { @@ -322,70 +347,47 @@ namespace DetourNavigator " generation=" << locked->getGeneration() << " revision=" << locked->getNavMeshRevision() << " time=" << std::chrono::duration_cast(finish - start).count() << "ms" << - " total_time=" << std::chrono::duration_cast(finish - firstStart).count() << "ms" " thread=" << std::this_thread::get_id(); return isSuccess(status); } - std::optional AsyncNavMeshUpdater::getNextJob() + JobIt AsyncNavMeshUpdater::getNextJob() { std::unique_lock lock(mMutex); - const auto threadId = std::this_thread::get_id(); - auto& threadQueue = mThreadsQueues[threadId]; - - while (true) + bool shouldStop = false; + const auto hasJob = [&] { - const auto hasJob = [&] { - return (!mJobs.empty() && mJobs.front().mProcessTime <= std::chrono::steady_clock::now()) - || !threadQueue.mJobs.empty(); - }; + shouldStop = mShouldStop; + return shouldStop + || (!mWaiting.empty() && mWaiting.front()->mProcessTime <= std::chrono::steady_clock::now()); + }; - if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) - { - mFirstStart.lock()->reset(); - if (mJobs.empty() && getTotalThreadJobsUnsafe() == 0) - mDone.notify_all(); - return std::nullopt; - } - - Log(Debug::Debug) << "Got " << mJobs.size() << " navigator jobs and " - << threadQueue.mJobs.size() << " thread jobs by thread=" << std::this_thread::get_id(); - - auto job = threadQueue.mJobs.empty() - ? getJob(mJobs, mPushed, true) - : getJob(threadQueue.mJobs, threadQueue.mPushed, false); - - if (!job) - continue; - - const auto owner = lockTile(job->mAgentHalfExtents, job->mChangedTile); - - if (owner == threadId) - return job; - - postThreadJob(std::move(*job), mThreadsQueues[owner]); + if (!mHasJob.wait_for(lock, std::chrono::milliseconds(10), hasJob)) + { + if (mJobs.empty()) + mDone.notify_all(); + return mJobs.end(); } - } - std::optional AsyncNavMeshUpdater::getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate) - { - const auto now = std::chrono::steady_clock::now(); + if (shouldStop) + return mJobs.end(); - if (jobs.front().mProcessTime > now) - return {}; + const JobIt job = mWaiting.front(); - Job job = jobs.front(); - jobs.pop_front(); + mWaiting.pop_front(); - if (changeLastUpdate && job.mChangeType == ChangeType::update) - mLastUpdates[job.mAgentHalfExtents][job.mChangedTile] = now; + if (!lockTile(job->mAgentHalfExtents, job->mChangedTile)) + { + ++job->mTryNumber; + insertPrioritizedJob(job, mWaiting); + return mJobs.end(); + } - const auto it = pushed.find(job.mAgentHalfExtents); - it->second.erase(job.mChangedTile); - if (it->second.empty()) - pushed.erase(it); + if (job->mChangeType == ChangeType::update) + mLastUpdates[getAgentAndTile(*job)] = std::chrono::steady_clock::now(); + mPushed.erase(getAgentAndTile(*job)); return job; } @@ -413,84 +415,37 @@ namespace DetourNavigator writeToFile(shared->lockConst()->getImpl(), mSettings.get().mNavMeshPathPrefix, navMeshRevision); } - std::chrono::steady_clock::time_point AsyncNavMeshUpdater::setFirstStart(const std::chrono::steady_clock::time_point& value) + void AsyncNavMeshUpdater::repost(JobIt job) { - const auto locked = mFirstStart.lock(); - if (!*locked) - *locked = value; - return *locked.get(); - } - - void AsyncNavMeshUpdater::repost(Job&& job) - { - if (mShouldStop || job.mTryNumber > 2) + if (mShouldStop || job->mTryNumber > 2) return; const std::lock_guard lock(mMutex); - if (mPushed[job.mAgentHalfExtents].insert(job.mChangedTile).second) + if (mPushed.emplace(job->mAgentHalfExtents, job->mChangedTile).second) { - ++job.mTryNumber; - mJobs.push_back(std::move(job)); + ++job->mTryNumber; + insertPrioritizedJob(job, mWaiting); mHasJob.notify_all(); + return; } + + mJobs.erase(job); } - void AsyncNavMeshUpdater::postThreadJob(Job&& job, Queue& queue) - { - if (queue.mPushed[job.mAgentHalfExtents].insert(job.mChangedTile).second) - { - queue.mJobs.push_back(std::move(job)); - mHasJob.notify_all(); - } - } - - std::thread::id AsyncNavMeshUpdater::lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) + bool AsyncNavMeshUpdater::lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) { if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) - return std::this_thread::get_id(); - - auto locked = mProcessingTiles.lock(); - - auto agent = locked->find(agentHalfExtents); - if (agent == locked->end()) - { - const auto threadId = std::this_thread::get_id(); - locked->emplace(agentHalfExtents, std::map({{changedTile, threadId}})); - return threadId; - } - - auto tile = agent->second.find(changedTile); - if (tile == agent->second.end()) - { - const auto threadId = std::this_thread::get_id(); - agent->second.emplace(changedTile, threadId); - return threadId; - } - - return tile->second; + return true; + return mProcessingTiles.lock()->emplace(agentHalfExtents, changedTile).second; } void AsyncNavMeshUpdater::unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile) { if (mSettings.get().mAsyncNavMeshUpdaterThreads <= 1) return; - auto locked = mProcessingTiles.lock(); - - auto agent = locked->find(agentHalfExtents); - if (agent == locked->end()) - return; - - auto tile = agent->second.find(changedTile); - if (tile == agent->second.end()) - return; - - agent->second.erase(tile); - - if (agent->second.empty()) - locked->erase(agent); - + locked->erase(std::tie(agentHalfExtents, changedTile)); if (locked->empty()) mProcessed.notify_all(); } @@ -498,13 +453,7 @@ namespace DetourNavigator std::size_t AsyncNavMeshUpdater::getTotalJobs() const { const std::scoped_lock lock(mMutex); - return mJobs.size() + getTotalThreadJobsUnsafe(); - } - - std::size_t AsyncNavMeshUpdater::getTotalThreadJobsUnsafe() const - { - return std::accumulate(mThreadsQueues.begin(), mThreadsQueues.end(), std::size_t(0), - [] (auto r, const auto& v) { return r + v.second.mJobs.size(); }); + return mJobs.size(); } void AsyncNavMeshUpdater::cleanupLastUpdates() @@ -513,20 +462,18 @@ namespace DetourNavigator const std::lock_guard lock(mMutex); - for (auto agent = mLastUpdates.begin(); agent != mLastUpdates.end();) + for (auto it = mLastUpdates.begin(); it != mLastUpdates.end();) { - for (auto tile = agent->second.begin(); tile != agent->second.end();) - { - if (now - tile->second > mSettings.get().mMinUpdateInterval) - tile = agent->second.erase(tile); - else - ++tile; - } - - if (agent->second.empty()) - agent = mLastUpdates.erase(agent); + if (now - it->second > mSettings.get().mMinUpdateInterval) + it = mLastUpdates.erase(it); else - ++agent; + ++it; } } + + void AsyncNavMeshUpdater::removeJob(JobIt job) + { + const std::lock_guard lock(mMutex); + mJobs.erase(job); + } } diff --git a/components/detournavigator/asyncnavmeshupdater.hpp b/components/detournavigator/asyncnavmeshupdater.hpp index e8b2611e97..2d915ad434 100644 --- a/components/detournavigator/asyncnavmeshupdater.hpp +++ b/components/detournavigator/asyncnavmeshupdater.hpp @@ -19,6 +19,7 @@ #include #include #include +#include class dtNavMesh; @@ -52,6 +53,24 @@ namespace DetourNavigator return stream << "ChangeType::" << static_cast(value); } + struct Job + { + const osg::Vec3f mAgentHalfExtents; + const std::weak_ptr mNavMeshCacheItem; + const TilePosition mChangedTile; + const std::chrono::steady_clock::time_point mProcessTime; + unsigned mTryNumber = 0; + ChangeType mChangeType; + int mDistanceToPlayer; + const int mDistanceToOrigin; + + Job(const osg::Vec3f& agentHalfExtents, std::weak_ptr navMeshCacheItem, + const TilePosition& changedTile, ChangeType changeType, int distanceToPlayer, + std::chrono::steady_clock::time_point processTime); + }; + + using JobIt = std::list::iterator; + class AsyncNavMeshUpdater { public: @@ -67,39 +86,6 @@ namespace DetourNavigator void reportStats(unsigned int frameNumber, osg::Stats& stats) const; private: - struct Job - { - osg::Vec3f mAgentHalfExtents; - std::weak_ptr mNavMeshCacheItem; - TilePosition mChangedTile; - unsigned mTryNumber; - ChangeType mChangeType; - int mDistanceToPlayer; - int mDistanceToOrigin; - std::chrono::steady_clock::time_point mProcessTime; - - std::tuple getPriority() const - { - return std::make_tuple(mProcessTime, mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin); - } - - friend inline bool operator <(const Job& lhs, const Job& rhs) - { - return lhs.getPriority() < rhs.getPriority(); - } - }; - - using Jobs = std::deque; - using Pushed = std::map>; - - struct Queue - { - Jobs mJobs; - Pushed mPushed; - - Queue() = default; - }; - std::reference_wrapper mSettings; std::reference_wrapper mRecastMeshManager; std::reference_wrapper mOffMeshConnectionsManager; @@ -108,46 +94,43 @@ namespace DetourNavigator std::condition_variable mHasJob; std::condition_variable mDone; std::condition_variable mProcessed; - Jobs mJobs; - std::map> mPushed; + std::list mJobs; + std::deque mWaiting; + std::set> mPushed; Misc::ScopeGuarded mPlayerTile; - Misc::ScopeGuarded> mFirstStart; NavMeshTilesCache mNavMeshTilesCache; - Misc::ScopeGuarded>> mProcessingTiles; - std::map> mLastUpdates; + Misc::ScopeGuarded>> mProcessingTiles; + std::map, std::chrono::steady_clock::time_point> mLastUpdates; std::set> mPresentTiles; - std::map mThreadsQueues; std::vector mThreads; void process() noexcept; bool processJob(const Job& job); - std::optional getNextJob(); + JobIt getNextJob(); - std::optional getJob(Jobs& jobs, Pushed& pushed, bool changeLastUpdate); + JobIt getJob(std::deque& jobs, bool changeLastUpdate); - void postThreadJob(Job&& job, Queue& queue); + void postThreadJob(JobIt job, std::deque& queue); void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const; - std::chrono::steady_clock::time_point setFirstStart(const std::chrono::steady_clock::time_point& value); + void repost(JobIt job); - void repost(Job&& job); - - std::thread::id lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); + bool lockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); void unlockTile(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile); inline std::size_t getTotalJobs() const; - inline std::size_t getTotalThreadJobsUnsafe() const; - void cleanupLastUpdates(); int waitUntilJobsDoneForNotPresentTiles(const std::size_t initialJobsLeft, std::size_t& maxJobsLeft, Loading::Listener& listener); void waitUntilAllJobsDone(); + + inline void removeJob(JobIt job); }; } diff --git a/components/detournavigator/cachedrecastmeshmanager.cpp b/components/detournavigator/cachedrecastmeshmanager.cpp index 4666b66a83..19b87aa820 100644 --- a/components/detournavigator/cachedrecastmeshmanager.cpp +++ b/components/detournavigator/cachedrecastmeshmanager.cpp @@ -8,12 +8,12 @@ namespace DetourNavigator : mImpl(settings, bounds, generation) {} - bool CachedRecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, + bool CachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { if (!mImpl.addObject(id, shape, transform, areaType)) return false; - mCached.reset(); + mCached.lock()->reset(); return true; } @@ -21,40 +21,60 @@ namespace DetourNavigator { if (!mImpl.updateObject(id, transform, areaType)) return false; - mCached.reset(); + mCached.lock()->reset(); return true; } std::optional CachedRecastMeshManager::removeObject(const ObjectId id) { - const auto object = mImpl.removeObject(id); + auto object = mImpl.removeObject(id); if (object) - mCached.reset(); + mCached.lock()->reset(); return object; } bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + const osg::Vec3f& shift) { - if (!mImpl.addWater(cellPosition, cellSize, transform)) + if (!mImpl.addWater(cellPosition, cellSize, shift)) return false; - mCached.reset(); + mCached.lock()->reset(); return true; } - std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto water = mImpl.removeWater(cellPosition); if (water) - mCached.reset(); + mCached.lock()->reset(); return water; } + bool CachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, + const osg::Vec3f& shift, const HeightfieldShape& shape) + { + if (!mImpl.addHeightfield(cellPosition, cellSize, shift, shape)) + return false; + mCached.lock()->reset(); + return true; + } + + std::optional CachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const auto cell = mImpl.removeHeightfield(cellPosition); + if (cell) + mCached.lock()->reset(); + return cell; + } + std::shared_ptr CachedRecastMeshManager::getMesh() { - if (!mCached) - mCached = mImpl.getMesh(); - return mCached; + std::shared_ptr cached = *mCached.lock(); + if (cached != nullptr) + return cached; + cached = mImpl.getMesh(); + *mCached.lock() = cached; + return cached; } bool CachedRecastMeshManager::isEmpty() const diff --git a/components/detournavigator/cachedrecastmeshmanager.hpp b/components/detournavigator/cachedrecastmeshmanager.hpp index 96604c3628..b506f807fa 100644 --- a/components/detournavigator/cachedrecastmeshmanager.hpp +++ b/components/detournavigator/cachedrecastmeshmanager.hpp @@ -3,6 +3,9 @@ #include "recastmeshmanager.hpp" #include "version.hpp" +#include "heightfieldshape.hpp" + +#include namespace DetourNavigator { @@ -11,17 +14,22 @@ namespace DetourNavigator public: CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); - - std::optional removeWater(const osg::Vec2i& cellPosition); - std::optional removeObject(const ObjectId id); + bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + + std::optional removeWater(const osg::Vec2i& cellPosition); + + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape); + + std::optional removeHeightfield(const osg::Vec2i& cellPosition); + std::shared_ptr getMesh(); bool isEmpty() const; @@ -32,7 +40,7 @@ namespace DetourNavigator private: RecastMeshManager mImpl; - std::shared_ptr mCached; + Misc::ScopeGuarded> mCached; }; } diff --git a/components/detournavigator/debug.cpp b/components/detournavigator/debug.cpp index c3d67b1848..4cb5b248b0 100644 --- a/components/detournavigator/debug.cpp +++ b/components/detournavigator/debug.cpp @@ -18,7 +18,7 @@ namespace DetourNavigator file.exceptions(std::ios::failbit | std::ios::badbit); file.precision(std::numeric_limits::max_exponent10); std::size_t count = 0; - for (auto v : recastMesh.getVertices()) + for (float v : recastMesh.getMesh().getVertices()) { if (count % 3 == 0) { @@ -31,7 +31,7 @@ namespace DetourNavigator } file << '\n'; count = 0; - for (auto v : recastMesh.getIndices()) + for (int v : recastMesh.getMesh().getIndices()) { if (count % 3 == 0) { diff --git a/components/detournavigator/debug.hpp b/components/detournavigator/debug.hpp index a17eec16a7..2128f96be4 100644 --- a/components/detournavigator/debug.hpp +++ b/components/detournavigator/debug.hpp @@ -34,6 +34,7 @@ namespace DetourNavigator switch (value) { OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(Success) + OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(PartialPath) OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(NavMeshNotFound) OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(StartPolygonNotFound) OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_STATUS_MESSAGE(EndPolygonNotFound) diff --git a/components/detournavigator/findrandompointaroundcircle.cpp b/components/detournavigator/findrandompointaroundcircle.cpp index ed73dca61a..a3407a61c3 100644 --- a/components/detournavigator/findrandompointaroundcircle.cpp +++ b/components/detournavigator/findrandompointaroundcircle.cpp @@ -19,7 +19,7 @@ namespace DetourNavigator dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); - dtPolyRef startRef = findNearestPolyExpanding(navMeshQuery, queryFilter, start, halfExtents); + dtPolyRef startRef = findNearestPoly(navMeshQuery, queryFilter, start, halfExtents * 4); if (startRef == 0) return std::optional(); diff --git a/components/detournavigator/findsmoothpath.cpp b/components/detournavigator/findsmoothpath.cpp index 6598263398..14fe696f10 100644 --- a/components/detournavigator/findsmoothpath.cpp +++ b/components/detournavigator/findsmoothpath.cpp @@ -146,16 +146,13 @@ namespace DetourNavigator return result; } - dtPolyRef findNearestPolyExpanding(const dtNavMeshQuery& query, const dtQueryFilter& filter, + dtPolyRef findNearestPoly(const dtNavMeshQuery& query, const dtQueryFilter& filter, const osg::Vec3f& center, const osg::Vec3f& halfExtents) { dtPolyRef ref = 0; - for (int i = 0; i < 3; ++i) - { - const dtStatus status = query.findNearestPoly(center.ptr(), (halfExtents * (1 << i)).ptr(), &filter, &ref, nullptr); - if (!dtStatusFailed(status) && ref != 0) - break; - } + const dtStatus status = query.findNearestPoly(center.ptr(), halfExtents.ptr(), &filter, &ref, nullptr); + if (!dtStatusSucceed(status)) + return 0; return ref; } } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index db2219b2cd..8eb0bf9f34 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -99,7 +99,7 @@ namespace DetourNavigator return dtStatusSucceed(status); } - dtPolyRef findNearestPolyExpanding(const dtNavMeshQuery& query, const dtQueryFilter& filter, + dtPolyRef findNearestPoly(const dtNavMeshQuery& query, const dtQueryFilter& filter, const osg::Vec3f& center, const osg::Vec3f& halfExtents); struct MoveAlongSurfaceResult @@ -256,7 +256,7 @@ namespace DetourNavigator template Status findSmoothPath(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, const float stepSize, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, - const Settings& settings, OutputIterator& out) + const Settings& settings, float endTolerance, OutputIterator& out) { dtNavMeshQuery navMeshQuery; if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes)) @@ -269,11 +269,15 @@ namespace DetourNavigator queryFilter.setAreaCost(AreaType_pathgrid, areaCosts.mPathgrid); queryFilter.setAreaCost(AreaType_ground, areaCosts.mGround); - dtPolyRef startRef = findNearestPolyExpanding(navMeshQuery, queryFilter, start, halfExtents); + constexpr float polyDistanceFactor = 4; + const osg::Vec3f polyHalfExtents = halfExtents * polyDistanceFactor; + + const dtPolyRef startRef = findNearestPoly(navMeshQuery, queryFilter, start, polyHalfExtents); if (startRef == 0) return Status::StartPolygonNotFound; - dtPolyRef endRef = findNearestPolyExpanding(navMeshQuery, queryFilter, end, halfExtents); + const dtPolyRef endRef = findNearestPoly(navMeshQuery, queryFilter, end, + polyHalfExtents + osg::Vec3f(endTolerance, endTolerance, endTolerance)); if (endRef == 0) return Status::EndPolygonNotFound; @@ -283,12 +287,18 @@ namespace DetourNavigator if (!polygonPath) return Status::FindPathOverPolygonsFailed; - if (polygonPath->empty() || polygonPath->back() != endRef) + if (polygonPath->empty()) return Status::Success; + const bool partialPath = polygonPath->back() != endRef; auto outTransform = OutputTransformIterator(out, settings); - return makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize, std::move(*polygonPath), - settings.mMaxSmoothPathSize, outTransform); + const Status smoothStatus = makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, stepSize, + std::move(*polygonPath), settings.mMaxSmoothPathSize, outTransform); + + if (smoothStatus != Status::Success) + return smoothStatus; + + return partialPath ? Status::PartialPath : Status::Success; } } diff --git a/components/detournavigator/gettilespositions.hpp b/components/detournavigator/gettilespositions.hpp index e233795e68..27c8f7a4ac 100644 --- a/components/detournavigator/gettilespositions.hpp +++ b/components/detournavigator/gettilespositions.hpp @@ -50,10 +50,11 @@ namespace DetourNavigator } template - void getTilesPositions(const int cellSize, const btTransform& transform, + void getTilesPositions(const int cellSize, const osg::Vec3f& shift, const Settings& settings, Callback&& callback) { const auto halfCellSize = cellSize / 2; + const btTransform transform(btMatrix3x3::getIdentity(), Misc::Convert::toBullet(shift)); auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0)); auto aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0)); diff --git a/components/detournavigator/heightfieldshape.hpp b/components/detournavigator/heightfieldshape.hpp new file mode 100644 index 0000000000..48a273725b --- /dev/null +++ b/components/detournavigator/heightfieldshape.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_HEIGHFIELDSHAPE_H + +#include +#include + +namespace DetourNavigator +{ + struct HeightfieldPlane + { + float mHeight; + }; + + struct HeightfieldSurface + { + const float* mHeights; + std::size_t mSize; + float mMinHeight; + float mMaxHeight; + }; + + using HeightfieldShape = std::variant; +} + +#endif diff --git a/components/detournavigator/makenavmesh.cpp b/components/detournavigator/makenavmesh.cpp index 422fdffb1d..3f133f5033 100644 --- a/components/detournavigator/makenavmesh.cpp +++ b/components/detournavigator/makenavmesh.cpp @@ -7,8 +7,12 @@ #include "sharednavmesh.hpp" #include "flags.hpp" #include "navmeshtilescache.hpp" +#include "preparednavmeshdata.hpp" +#include "navmeshdata.hpp" +#include "recastmeshbuilder.hpp" #include +#include #include #include @@ -26,51 +30,35 @@ namespace { using namespace DetourNavigator; - void initPolyMeshDetail(rcPolyMeshDetail& value) + struct Rectangle { - value.meshes = nullptr; - value.verts = nullptr; - value.tris = nullptr; - } - - struct PolyMeshDetailStackDeleter - { - void operator ()(rcPolyMeshDetail* value) const - { - rcFree(value->meshes); - rcFree(value->verts); - rcFree(value->tris); - } + TileBounds mBounds; + float mHeight; }; - using PolyMeshDetailStackPtr = std::unique_ptr; - - struct WaterBounds - { - osg::Vec3f mMin; - osg::Vec3f mMax; - }; - - WaterBounds getWaterBounds(const RecastMesh::Water& water, const Settings& settings, + Rectangle getSwimRectangle(const Cell& water, const Settings& settings, const osg::Vec3f& agentHalfExtents) { - if (water.mCellSize == std::numeric_limits::max()) + if (water.mSize == std::numeric_limits::max()) { - const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z()); - const auto min = toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(-1, -1, 0)))); - const auto max = toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(1, 1, 0)))); - return WaterBounds { - osg::Vec3f(-std::numeric_limits::max(), min.y(), -std::numeric_limits::max()), - osg::Vec3f(std::numeric_limits::max(), max.y(), std::numeric_limits::max()) + 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())) }; } else { - const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z()); - const auto halfCellSize = water.mCellSize / 2.0f; - return WaterBounds { - toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(-halfCellSize, -halfCellSize, 0)))), - toNavMeshCoordinates(settings, Misc::Convert::makeOsgVec3f(transform(btVector3(halfCellSize, halfCellSize, 0)))) + const osg::Vec2f shift(water.mShift.x(), water.mShift.y()); + const float halfCellSize = water.mSize / 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())) }; } } @@ -174,28 +162,34 @@ namespace throw NavigatorException("Failed to create heightfield for navmesh"); } - bool rasterizeSolidObjectsTriangles(rcContext& context, const RecastMesh& recastMesh, const rcConfig& config, + bool rasterizeTriangles(rcContext& context, const Mesh& mesh, const Settings& settings, const rcConfig& config, rcHeightfield& solid) { - const osg::Vec2f tileBoundsMin(config.bmin[0], config.bmin[2]); - const osg::Vec2f tileBoundsMax(config.bmax[0], config.bmax[2]); - std::vector areas(recastMesh.getAreaTypes().begin(), recastMesh.getAreaTypes().end()); + std::vector areas(mesh.getAreaTypes().begin(), mesh.getAreaTypes().end()); + std::vector vertices = mesh.getVertices(); + + for (std::size_t i = 0; i < vertices.size(); i += 3) + { + for (std::size_t j = 0; j < 3; ++j) + vertices[i + j] = toNavMeshCoordinates(settings, vertices[i + j]); + std::swap(vertices[i + 1], vertices[i + 2]); + } rcClearUnwalkableTriangles( &context, config.walkableSlopeAngle, - recastMesh.getVertices().data(), - static_cast(recastMesh.getVerticesCount()), - recastMesh.getIndices().data(), + vertices.data(), + static_cast(mesh.getVerticesCount()), + mesh.getIndices().data(), static_cast(areas.size()), areas.data() ); return rcRasterizeTriangles( &context, - recastMesh.getVertices().data(), - static_cast(recastMesh.getVerticesCount()), - recastMesh.getIndices().data(), + vertices.data(), + static_cast(mesh.getVerticesCount()), + mesh.getIndices().data(), areas.data(), static_cast(areas.size()), solid, @@ -203,70 +197,92 @@ namespace ); } - void rasterizeWaterTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, + bool rasterizeTriangles(rcContext& context, const Rectangle& rectangle, const rcConfig& config, + const unsigned char* areas, std::size_t areasSize, rcHeightfield& solid) + { + const osg::Vec2f tileBoundsMin( + std::clamp(rectangle.mBounds.mMin.x(), config.bmin[0], config.bmax[0]), + std::clamp(rectangle.mBounds.mMin.y(), config.bmin[2], config.bmax[2]) + ); + const osg::Vec2f tileBoundsMax( + std::clamp(rectangle.mBounds.mMax.x(), config.bmin[0], config.bmax[0]), + std::clamp(rectangle.mBounds.mMax.y(), config.bmin[2], config.bmax[2]) + ); + + if (tileBoundsMax == tileBoundsMin) + return true; + + const std::array vertices { + tileBoundsMin.x(), rectangle.mHeight, tileBoundsMin.y(), + tileBoundsMin.x(), rectangle.mHeight, tileBoundsMax.y(), + tileBoundsMax.x(), rectangle.mHeight, tileBoundsMax.y(), + tileBoundsMax.x(), rectangle.mHeight, tileBoundsMin.y(), + }; + + const std::array indices { + 0, 1, 2, + 0, 2, 3, + }; + + return rcRasterizeTriangles( + &context, + vertices.data(), + static_cast(vertices.size() / 3), + indices.data(), + areas, + static_cast(areasSize), + solid, + config.walkableClimb + ); + } + + bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const std::vector& cells, const Settings& settings, const rcConfig& config, rcHeightfield& solid) { const std::array areas {{AreaType_water, AreaType_water}}; - - for (const auto& water : recastMesh.getWater()) + for (const Cell& cell : cells) { - const auto bounds = getWaterBounds(water, settings, agentHalfExtents); - - const osg::Vec2f tileBoundsMin( - std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMin.x())), - std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMin.z())) - ); - const osg::Vec2f tileBoundsMax( - std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMax.x())), - std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMax.z())) - ); - - if (tileBoundsMax == tileBoundsMin) - continue; - - const std::array vertices {{ - osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMin.y()), - osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMax.y()), - osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMax.y()), - osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMin.y()), - }}; - - std::array convertedVertices; - auto convertedVerticesIt = convertedVertices.begin(); - - for (const auto& vertex : vertices) - convertedVerticesIt = std::copy(vertex.ptr(), vertex.ptr() + 3, convertedVerticesIt); - - const std::array indices {{ - 0, 1, 2, - 0, 2, 3, - }}; - - const auto trianglesRasterized = rcRasterizeTriangles( - &context, - convertedVertices.data(), - static_cast(convertedVertices.size() / 3), - indices.data(), - areas.data(), - static_cast(areas.size()), - solid, - config.walkableClimb - ); - - if (!trianglesRasterized) - throw NavigatorException("Failed to create rasterize water triangles for navmesh"); + const Rectangle rectangle = getSwimRectangle(cell, settings, agentHalfExtents); + if (!rasterizeTriangles(context, rectangle, config, areas.data(), areas.size(), solid)) + return false; } + return true; + } + + bool rasterizeTriangles(rcContext& context, 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; + } + return true; + } + + bool rasterizeTriangles(rcContext& context, const std::vector& heightfields, + const Settings& settings, const rcConfig& config, rcHeightfield& solid) + { + using BulletHelpers::makeProcessTriangleCallback; + + for (const Heightfield& heightfield : heightfields) + { + const Mesh mesh = makeMesh(heightfield); + if (!rasterizeTriangles(context, mesh, settings, config, solid)) + return false; + } + return true; } bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, const rcConfig& config, const Settings& settings, rcHeightfield& solid) { - if (!rasterizeSolidObjectsTriangles(context, recastMesh, config, solid)) - return false; - - rasterizeWaterTriangles(context, agentHalfExtents, recastMesh, settings, config, solid); - - return true; + 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); } void buildCompactHeightfield(rcContext& context, const int walkableHeight, const int walkableClimb, @@ -363,10 +379,25 @@ namespace return true; } - NavMeshData makeNavMeshTileData(const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh, - const std::vector& offMeshConnections, const TilePosition& tile, - const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax, const Settings& settings) + template + unsigned long getMinValuableBitsNumber(const T value) { + unsigned long power = 0; + while (power < sizeof(T) * 8 && (static_cast(1) << power) < value) + ++power; + return power; + } +} + +namespace DetourNavigator +{ + std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, + const TilePosition& tile, const Bounds& bounds, 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()); + rcContext context; const auto config = makeConfig(agentHalfExtents, boundsMin, boundsMax, settings); @@ -374,19 +405,27 @@ namespace createHeightfield(context, solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch); if (!rasterizeTriangles(context, agentHalfExtents, recastMesh, config, settings, solid)) - return NavMeshData(); + return nullptr; rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, solid); rcFilterLedgeSpans(&context, config.walkableHeight, config.walkableClimb, solid); rcFilterWalkableLowHeightSpans(&context, config.walkableHeight, solid); - rcPolyMesh polyMesh; - rcPolyMeshDetail polyMeshDetail; - initPolyMeshDetail(polyMeshDetail); - const PolyMeshDetailStackPtr polyMeshDetailPtr(&polyMeshDetail); - if (!fillPolyMesh(context, config, solid, polyMesh, polyMeshDetail)) - return NavMeshData(); + std::unique_ptr result = std::make_unique(); + if (!fillPolyMesh(context, config, solid, result->mPolyMesh, result->mPolyMeshDetail)) + return nullptr; + + result->mCellSize = config.cs; + result->mCellHeight = config.ch; + + return result; + } + + NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data, + const std::vector& offMeshConnections, const osg::Vec3f& agentHalfExtents, + const TilePosition& tile, const Settings& settings) + { const auto offMeshConVerts = getOffMeshVerts(offMeshConnections); const std::vector offMeshConRad(offMeshConnections.size(), getRadius(settings, agentHalfExtents)); const std::vector offMeshConDir(offMeshConnections.size(), 0); @@ -394,18 +433,18 @@ namespace const std::vector offMeshConFlags = getOffMeshFlags(offMeshConnections); dtNavMeshCreateParams params; - params.verts = polyMesh.verts; - params.vertCount = polyMesh.nverts; - params.polys = polyMesh.polys; - params.polyAreas = polyMesh.areas; - params.polyFlags = polyMesh.flags; - params.polyCount = polyMesh.npolys; - params.nvp = polyMesh.nvp; - params.detailMeshes = polyMeshDetail.meshes; - params.detailVerts = polyMeshDetail.verts; - params.detailVertsCount = polyMeshDetail.nverts; - params.detailTris = polyMeshDetail.tris; - params.detailTriCount = polyMeshDetail.ntris; + params.verts = data.mPolyMesh.verts; + params.vertCount = data.mPolyMesh.nverts; + params.polys = data.mPolyMesh.polys; + params.polyAreas = data.mPolyMesh.areas; + params.polyFlags = data.mPolyMesh.flags; + params.polyCount = data.mPolyMesh.npolys; + params.nvp = data.mPolyMesh.nvp; + params.detailMeshes = data.mPolyMeshDetail.meshes; + params.detailVerts = data.mPolyMeshDetail.verts; + params.detailVertsCount = data.mPolyMeshDetail.nverts; + params.detailTris = data.mPolyMeshDetail.tris; + params.detailTriCount = data.mPolyMeshDetail.ntris; params.offMeshConVerts = offMeshConVerts.data(); params.offMeshConRad = offMeshConRad.data(); params.offMeshConDir = offMeshConDir.data(); @@ -416,12 +455,12 @@ namespace params.walkableHeight = getHeight(settings, agentHalfExtents); params.walkableRadius = getRadius(settings, agentHalfExtents); params.walkableClimb = getMaxClimb(settings); - rcVcopy(params.bmin, polyMesh.bmin); - rcVcopy(params.bmax, polyMesh.bmax); - params.cs = config.cs; - params.ch = config.ch; + rcVcopy(params.bmin, data.mPolyMesh.bmin); + rcVcopy(params.bmax, data.mPolyMesh.bmax); + params.cs = data.mCellSize; + params.ch = data.mCellHeight; params.buildBvTree = true; - params.userId = 0; + params.userId = data.mUserId; params.tileX = tile.x(); params.tileY = tile.y(); params.tileLayer = 0; @@ -436,20 +475,6 @@ namespace return NavMeshData(navMeshData, navMeshDataSize); } - - - template - unsigned long getMinValuableBitsNumber(const T value) - { - unsigned long power = 0; - while (power < sizeof(T) * 8 && (static_cast(1) << power) < value) - ++power; - return power; - } -} - -namespace DetourNavigator -{ NavMeshPtr makeEmptyNavMesh(const Settings& settings) { // Max tiles and max polys affect how the tile IDs are caculated. @@ -470,6 +495,10 @@ namespace DetourNavigator params.maxPolys = 1 << polysBits; NavMeshPtr navMesh(dtAllocNavMesh(), &dtFreeNavMesh); + + if (navMesh == nullptr) + throw NavigatorException("Failed to allocate navmesh"); + const auto status = navMesh->init(¶ms); if (!dtStatusSucceed(status)) @@ -481,7 +510,7 @@ namespace DetourNavigator UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, const TilePosition& changedTile, const TilePosition& playerTile, const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache) + const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType) { Log(Debug::Debug) << std::fixed << std::setprecision(2) << "Update NavMesh with multiple tiles:" << @@ -492,9 +521,6 @@ namespace DetourNavigator " playerTile=(" << playerTile << ")" << " changedTileDistance=" << getDistance(changedTile, playerTile); - const auto params = *navMeshCacheItem->lockConst()->getImpl().getParams(); - const osg::Vec3f origin(params.orig[0], params.orig[1], params.orig[2]); - if (!recastMesh) { Log(Debug::Debug) << "Ignore add tile: recastMesh is null"; @@ -502,12 +528,14 @@ namespace DetourNavigator } auto recastMeshBounds = recastMesh->getBounds(); + recastMeshBounds.mMin = toNavMeshCoordinates(settings, recastMeshBounds.mMin); + recastMeshBounds.mMax = toNavMeshCoordinates(settings, recastMeshBounds.mMax); for (const auto& water : recastMesh->getWater()) { - const auto waterBounds = getWaterBounds(water, settings, agentHalfExtents); - recastMeshBounds.mMin.y() = std::min(recastMeshBounds.mMin.y(), waterBounds.mMin.y()); - recastMeshBounds.mMax.y() = std::max(recastMeshBounds.mMax.y(), waterBounds.mMax.y()); + 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)) @@ -516,41 +544,44 @@ namespace DetourNavigator return navMeshCacheItem->lock()->removeTile(changedTile); } + const dtNavMeshParams params = *navMeshCacheItem->lockConst()->getImpl().getParams(); + if (!shouldAddTile(changedTile, playerTile, std::min(settings.mMaxTilesNumber, params.maxTiles))) { Log(Debug::Debug) << "Ignore add tile: too far from player"; return navMeshCacheItem->lock()->removeTile(changedTile); } - auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh, offMeshConnections); + auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh); bool cached = static_cast(cachedNavMeshData); if (!cachedNavMeshData) { - const auto tileBounds = makeTileBounds(settings, changedTile); - const osg::Vec3f tileBorderMin(tileBounds.mMin.x(), recastMeshBounds.mMin.y() - 1, tileBounds.mMin.y()); - const osg::Vec3f tileBorderMax(tileBounds.mMax.x(), recastMeshBounds.mMax.y() + 1, tileBounds.mMax.y()); + auto prepared = prepareNavMeshTileData(*recastMesh, changedTile, recastMeshBounds, + agentHalfExtents, settings); - auto navMeshData = makeNavMeshTileData(agentHalfExtents, *recastMesh, offMeshConnections, changedTile, - tileBorderMin, tileBorderMax, settings); - - if (!navMeshData.mValue) + if (prepared == nullptr) { Log(Debug::Debug) << "Ignore add tile: NavMeshData is null"; return navMeshCacheItem->lock()->removeTile(changedTile); } - cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, - offMeshConnections, std::move(navMeshData)); + if (updateType == UpdateType::Temporary) + return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(), + makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings)); + + cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh, std::move(prepared)); if (!cachedNavMeshData) { Log(Debug::Debug) << "Navigator cache overflow"; - return navMeshCacheItem->lock()->updateTile(changedTile, std::move(navMeshData)); + return navMeshCacheItem->lock()->updateTile(changedTile, NavMeshTilesCache::Value(), + makeNavMeshTileData(*prepared, offMeshConnections, agentHalfExtents, changedTile, settings)); } } - const auto updateStatus = navMeshCacheItem->lock()->updateTile(changedTile, std::move(cachedNavMeshData)); + const auto updateStatus = navMeshCacheItem->lock()->updateTile(changedTile, std::move(cachedNavMeshData), + makeNavMeshTileData(cachedNavMeshData.get(), offMeshConnections, agentHalfExtents, changedTile, settings)); return UpdateNavMeshStatusBuilder(updateStatus).cached(cached).getResult(); } diff --git a/components/detournavigator/makenavmesh.hpp b/components/detournavigator/makenavmesh.hpp index 95720634cd..5b4169374b 100644 --- a/components/detournavigator/makenavmesh.hpp +++ b/components/detournavigator/makenavmesh.hpp @@ -6,10 +6,12 @@ #include "tileposition.hpp" #include "sharednavmesh.hpp" #include "navmeshtilescache.hpp" +#include "offmeshconnection.hpp" #include #include +#include class dtNavMesh; @@ -17,6 +19,8 @@ namespace DetourNavigator { class RecastMesh; struct Settings; + struct PreparedNavMeshData; + struct NavMeshData; inline float getLength(const osg::Vec2i& value) { @@ -34,12 +38,25 @@ namespace DetourNavigator return expectedTilesCount <= maxTiles; } + std::unique_ptr prepareNavMeshTileData(const RecastMesh& recastMesh, const TilePosition& tile, + const Bounds& bounds, const osg::Vec3f& agentHalfExtents, const Settings& settings); + + NavMeshData makeNavMeshTileData(const PreparedNavMeshData& data, + const std::vector& offMeshConnections, const osg::Vec3f& agentHalfExtents, + const TilePosition& tile, const Settings& settings); + NavMeshPtr makeEmptyNavMesh(const Settings& settings); + enum class UpdateType + { + Persistent, + Temporary + }; + UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh, const TilePosition& changedTile, const TilePosition& playerTile, const std::vector& offMeshConnections, const Settings& settings, - const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache); + const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache, UpdateType updateType); } #endif diff --git a/components/detournavigator/navigator.hpp b/components/detournavigator/navigator.hpp index 0dab7ba6b3..d2e77892b9 100644 --- a/components/detournavigator/navigator.hpp +++ b/components/detournavigator/navigator.hpp @@ -8,6 +8,11 @@ #include "navmeshcacheitem.hpp" #include "recastmeshtiles.hpp" #include "waitconditiontype.hpp" +#include "heightfieldshape.hpp" + +#include + +#include namespace ESM { @@ -24,11 +29,10 @@ namespace DetourNavigator { struct ObjectShapes { - const btCollisionShape& mShape; - const btCollisionShape* mAvoid; + osg::ref_ptr mShapeInstance; - ObjectShapes(const btCollisionShape& shape, const btCollisionShape* avoid) - : mShape(shape), mAvoid(avoid) + ObjectShapes(const osg::ref_ptr& shapeInstance) + : mShapeInstance(shapeInstance) {} }; @@ -37,9 +41,9 @@ namespace DetourNavigator osg::Vec3f mConnectionStart; osg::Vec3f mConnectionEnd; - DoorShapes(const btCollisionShape& shape, const btCollisionShape* avoid, + DoorShapes(const osg::ref_ptr& shapeInstance, const osg::Vec3f& connectionStart,const osg::Vec3f& connectionEnd) - : ObjectShapes(shape, avoid) + : ObjectShapes(shapeInstance) , mConnectionStart(connectionStart) , mConnectionEnd(connectionEnd) {} @@ -69,15 +73,6 @@ namespace DetourNavigator */ virtual void removeAgent(const osg::Vec3f& agentHalfExtents) = 0; - /** - * @brief addObject is used to add object represented by single btCollisionShape and btTransform. - * @param id is used to distinguish different objects. - * @param shape must live until object is updated by another shape removed from Navigator. - * @param transform allows to setup object geometry according to its world state. - * @return true if object is added, false if there is already object with given id. - */ - virtual bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) = 0; - /** * @brief addObject is used to add complex object with allowed to walk and avoided to walk shapes * @param id is used to distinguish different objects @@ -96,15 +91,6 @@ namespace DetourNavigator */ virtual bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) = 0; - /** - * @brief updateObject replace object geometry by given data. - * @param id is used to find object. - * @param shape must live until object is updated by another shape removed from Navigator. - * @param transform allows to setup objects geometry according to its world state. - * @return true if object is updated, false if there is no object with given id. - */ - virtual bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) = 0; - /** * @brief updateObject replace object geometry by given data. * @param id is used to find object. @@ -134,14 +120,12 @@ namespace DetourNavigator * @brief addWater is used to set water level at given world cell. * @param cellPosition allows to distinguish cells if there is many in current world. * @param cellSize set cell borders. std::numeric_limits::max() disables cell borders. - * @param level set z coordinate of water surface at the scene. - * @param transform set global shift of cell center. + * @param shift set global shift of cell center. * @return true if there was no water at given cell if cellSize != std::numeric_limits::max() or there is * at least single object is added to the scene, false if there is already water for given cell or there is no * any other objects. */ - virtual bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) = 0; + virtual bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) = 0; /** * @brief removeWater to make it no more available at the scene. @@ -150,6 +134,11 @@ 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 removeHeightfield(const osg::Vec2i& cellPosition) = 0; + virtual void addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) = 0; virtual void removePathgrid(const ESM::Pathgrid& pathgrid) = 0; @@ -184,13 +173,14 @@ namespace DetourNavigator * @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, - OutputIterator& out) const + float endTolerance, OutputIterator& out) const { static_assert( std::is_same< @@ -205,7 +195,7 @@ namespace DetourNavigator const auto settings = getSettings(); return findSmoothPath(navMesh->lockConst()->getImpl(), toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, stepSize), toNavMeshCoordinates(settings, start), - toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, out); + toNavMeshCoordinates(settings, end), includeFlags, areaCosts, settings, endTolerance, out); } /** diff --git a/components/detournavigator/navigatorimpl.cpp b/components/detournavigator/navigatorimpl.cpp index 1aa4768de5..750901c73e 100644 --- a/components/detournavigator/navigatorimpl.cpp +++ b/components/detournavigator/navigatorimpl.cpp @@ -2,6 +2,7 @@ #include "debug.hpp" #include "settingsutils.hpp" +#include #include #include @@ -31,18 +32,15 @@ namespace DetourNavigator --it->second; } - bool NavigatorImpl::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) - { - return mNavMeshManager.addObject(id, shape, transform, AreaType_ground); - } - bool NavigatorImpl::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - bool result = addObject(id, shapes.mShape, transform); - if (shapes.mAvoid) + CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + bool result = mNavMeshManager.addObject(id, collisionShape, transform, AreaType_ground); + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) { - const ObjectId avoidId(shapes.mAvoid); - if (mNavMeshManager.addObject(avoidId, *shapes.mAvoid, transform, AreaType_null)) + const ObjectId avoidId(avoidShape); + CollisionShape avoidCollisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.addObject(avoidId, avoidCollisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; @@ -64,18 +62,15 @@ namespace DetourNavigator return false; } - bool NavigatorImpl::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) - { - return mNavMeshManager.updateObject(id, shape, transform, AreaType_ground); - } - bool NavigatorImpl::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) { - bool result = updateObject(id, shapes.mShape, transform); - if (shapes.mAvoid) + const CollisionShape collisionShape {shapes.mShapeInstance, *shapes.mShapeInstance->getCollisionShape()}; + bool result = mNavMeshManager.updateObject(id, collisionShape, transform, AreaType_ground); + if (const btCollisionShape* const avoidShape = shapes.mShapeInstance->getAvoidCollisionShape()) { - const ObjectId avoidId(shapes.mAvoid); - if (mNavMeshManager.updateObject(avoidId, *shapes.mAvoid, transform, AreaType_null)) + const ObjectId avoidId(avoidShape); + const CollisionShape collisionShape {shapes.mShapeInstance, *avoidShape}; + if (mNavMeshManager.updateObject(avoidId, collisionShape, transform, AreaType_null)) { updateAvoidShapeId(id, avoidId); result = true; @@ -102,11 +97,9 @@ namespace DetourNavigator return result; } - bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) + bool NavigatorImpl::addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) { - return mNavMeshManager.addWater(cellPosition, cellSize, - btTransform(transform.getBasis(), btVector3(transform.getOrigin().x(), transform.getOrigin().y(), level))); + return mNavMeshManager.addWater(cellPosition, cellSize, shift); } bool NavigatorImpl::removeWater(const osg::Vec2i& cellPosition) @@ -114,6 +107,17 @@ namespace DetourNavigator return mNavMeshManager.removeWater(cellPosition); } + bool NavigatorImpl::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape) + { + return mNavMeshManager.addHeightfield(cellPosition, cellSize, shift, shape); + } + + bool NavigatorImpl::removeHeightfield(const osg::Vec2i& cellPosition) + { + return mNavMeshManager.removeHeightfield(cellPosition); + } + void NavigatorImpl::addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) { Misc::CoordinateConverter converter(&cell); diff --git a/components/detournavigator/navigatorimpl.hpp b/components/detournavigator/navigatorimpl.hpp index 80c6957d79..cda6158958 100644 --- a/components/detournavigator/navigatorimpl.hpp +++ b/components/detournavigator/navigatorimpl.hpp @@ -21,25 +21,25 @@ namespace DetourNavigator void removeAgent(const osg::Vec3f& agentHalfExtents) override; - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) override; - bool addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) override; bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) override; - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform) override; - bool updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform) override; bool updateObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform) override; bool removeObject(const ObjectId id) override; - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level, - const btTransform& transform) override; + bool addWater(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift) 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 removeHeightfield(const osg::Vec2i& cellPosition) override; + void addPathgrid(const ESM::Cell& cell, const ESM::Pathgrid& pathgrid) override; void removePathgrid(const ESM::Pathgrid& pathgrid) override; diff --git a/components/detournavigator/navigatorstub.hpp b/components/detournavigator/navigatorstub.hpp index 0a08813938..40de90f256 100644 --- a/components/detournavigator/navigatorstub.hpp +++ b/components/detournavigator/navigatorstub.hpp @@ -19,11 +19,6 @@ namespace DetourNavigator void removeAgent(const osg::Vec3f& /*agentHalfExtents*/) override {} - bool addObject(const ObjectId /*id*/, const btCollisionShape& /*shape*/, const btTransform& /*transform*/) override - { - return false; - } - bool addObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; @@ -34,11 +29,6 @@ namespace DetourNavigator return false; } - bool updateObject(const ObjectId /*id*/, const btCollisionShape& /*shape*/, const btTransform& /*transform*/) override - { - return false; - } - bool updateObject(const ObjectId /*id*/, const ObjectShapes& /*shapes*/, const btTransform& /*transform*/) override { return false; @@ -54,8 +44,7 @@ namespace DetourNavigator return false; } - bool addWater(const osg::Vec2i& /*cellPosition*/, const int /*cellSize*/, const btScalar /*level*/, - const btTransform& /*transform*/) override + bool addWater(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/) override { return false; } @@ -65,6 +54,17 @@ namespace DetourNavigator return false; } + bool addHeightfield(const osg::Vec2i& /*cellPosition*/, int /*cellSize*/, const osg::Vec3f& /*shift*/, + const HeightfieldShape& /*height*/) override + { + return false; + } + + bool removeHeightfield(const osg::Vec2i& /*cellPosition*/) override + { + return false; + } + void addPathgrid(const ESM::Cell& /*cell*/, const ESM::Pathgrid& /*pathgrid*/) override {} void removePathgrid(const ESM::Pathgrid& /*pathgrid*/) override {} diff --git a/components/detournavigator/navmeshcacheitem.cpp b/components/detournavigator/navmeshcacheitem.cpp new file mode 100644 index 0000000000..ee6f3308d0 --- /dev/null +++ b/components/detournavigator/navmeshcacheitem.cpp @@ -0,0 +1,82 @@ +#include "tileposition.hpp" +#include "navmeshtilescache.hpp" +#include "dtstatus.hpp" +#include "navmeshtileview.hpp" +#include "navmeshcacheitem.hpp" +#include "navmeshdata.hpp" + +#include + +#include + +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; + const auto tileRef = navMesh.getTileRefAt(position.x(), position.y(), layer); + if (tileRef == 0) + return false; + unsigned char** const data = nullptr; + int* const dataSize = nullptr; + return dtStatusSucceed(navMesh.removeTile(tileRef, data, dataSize)); + } + + dtStatus addTile(dtNavMesh& navMesh, unsigned char* data, int size) + { + const int doNotTransferOwnership = 0; + const dtTileRef lastRef = 0; + dtTileRef* const result = nullptr; + return navMesh.addTile(data, size, doNotTransferOwnership, lastRef, result); + } +} + +namespace DetourNavigator +{ + UpdateNavMeshStatus NavMeshCacheItem::updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, + NavMeshData&& navMeshData) + { + const dtMeshTile* currentTile = ::getTile(*mImpl, position); + if (currentTile != nullptr + && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(navMeshData.mValue.get())) + { + return UpdateNavMeshStatus::ignored; + } + const auto removed = ::removeTile(*mImpl, position); + 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; + return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult(); + } + else + { + if (removed) + { + mUsedTiles.erase(position); + ++mNavMeshRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult(); + } + } + + UpdateNavMeshStatus NavMeshCacheItem::removeTile(const TilePosition& position) + { + const auto removed = ::removeTile(*mImpl, position); + if (removed) + { + mUsedTiles.erase(position); + ++mNavMeshRevision; + } + return UpdateNavMeshStatusBuilder().removed(removed).getResult(); + } +} diff --git a/components/detournavigator/navmeshcacheitem.hpp b/components/detournavigator/navmeshcacheitem.hpp index a9a4ebee62..ac68caedb3 100644 --- a/components/detournavigator/navmeshcacheitem.hpp +++ b/components/detournavigator/navmeshcacheitem.hpp @@ -5,13 +5,14 @@ #include "tileposition.hpp" #include "navmeshtilescache.hpp" #include "dtstatus.hpp" -#include "navmeshtileview.hpp" +#include "navmeshdata.hpp" #include -#include - #include +#include + +struct dtMeshTile; namespace DetourNavigator { @@ -33,6 +34,32 @@ namespace DetourNavigator return (static_cast(value) & static_cast(UpdateNavMeshStatus::failed)) == 0; } + inline std::ostream& operator <<(std::ostream& stream, UpdateNavMeshStatus value) + { + switch (value) + { + case UpdateNavMeshStatus::ignored: + return stream << "ignore"; + case UpdateNavMeshStatus::removed: + return stream << "removed"; + case UpdateNavMeshStatus::added: + return stream << "add"; + case UpdateNavMeshStatus::replaced: + return stream << "replaced"; + case UpdateNavMeshStatus::failed: + return stream << "failed"; + case UpdateNavMeshStatus::lost: + return stream << "lost"; + case UpdateNavMeshStatus::cached: + return stream << "cached"; + case UpdateNavMeshStatus::unchanged: + return stream << "unchanged"; + case UpdateNavMeshStatus::restored: + return stream << "restored"; + } + return stream << "unknown(" << static_cast(value) << ")"; + } + class UpdateNavMeshStatusBuilder { public: @@ -96,26 +123,6 @@ namespace DetourNavigator } }; - inline unsigned char* getRawData(NavMeshData& navMeshData) - { - return navMeshData.mValue.get(); - } - - inline unsigned char* getRawData(NavMeshTilesCache::Value& cachedNavMeshData) - { - return cachedNavMeshData.get().mValue; - } - - inline int getSize(const NavMeshData& navMeshData) - { - return navMeshData.mSize; - } - - inline int getSize(const NavMeshTilesCache::Value& cachedNavMeshData) - { - return cachedNavMeshData.get().mSize; - } - class NavMeshCacheItem { public: @@ -139,86 +146,16 @@ namespace DetourNavigator return mNavMeshRevision; } - template - UpdateNavMeshStatus updateTile(const TilePosition& position, T&& navMeshData) - { - const dtMeshTile* currentTile = getTile(position); - if (currentTile != nullptr - && asNavMeshTileConstView(*currentTile) == asNavMeshTileConstView(getRawData(navMeshData))) - { - return UpdateNavMeshStatus::ignored; - } - const auto removed = removeTileImpl(position); - const auto addStatus = addTileImpl(getRawData(navMeshData), getSize(navMeshData)); - if (dtStatusSucceed(addStatus)) - { - setUsedTile(position, std::forward(navMeshData)); - return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult(); - } - else - { - if (removed) - removeUsedTile(position); - return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult(); - } - } + UpdateNavMeshStatus updateTile(const TilePosition& position, NavMeshTilesCache::Value&& cached, + NavMeshData&& navMeshData); - UpdateNavMeshStatus removeTile(const TilePosition& position) - { - const auto removed = removeTileImpl(position); - if (removed) - removeUsedTile(position); - return UpdateNavMeshStatusBuilder().removed(removed).getResult(); - } + UpdateNavMeshStatus removeTile(const TilePosition& position); private: NavMeshPtr mImpl; std::size_t mGeneration; std::size_t mNavMeshRevision; std::map> mUsedTiles; - - void setUsedTile(const TilePosition& tilePosition, NavMeshTilesCache::Value value) - { - mUsedTiles[tilePosition] = std::make_pair(std::move(value), NavMeshData()); - ++mNavMeshRevision; - } - - void setUsedTile(const TilePosition& tilePosition, NavMeshData value) - { - mUsedTiles[tilePosition] = std::make_pair(NavMeshTilesCache::Value(), std::move(value)); - ++mNavMeshRevision; - } - - void removeUsedTile(const TilePosition& tilePosition) - { - mUsedTiles.erase(tilePosition); - ++mNavMeshRevision; - } - - dtStatus addTileImpl(unsigned char* data, int size) - { - const int doNotTransferOwnership = 0; - const dtTileRef lastRef = 0; - dtTileRef* const result = nullptr; - return mImpl->addTile(data, size, doNotTransferOwnership, lastRef, result); - } - - bool removeTileImpl(const TilePosition& position) - { - const int layer = 0; - const auto tileRef = mImpl->getTileRefAt(position.x(), position.y(), layer); - if (tileRef == 0) - return false; - unsigned char** const data = nullptr; - int* const dataSize = nullptr; - return dtStatusSucceed(mImpl->removeTile(tileRef, data, dataSize)); - } - - const dtMeshTile* getTile(const TilePosition& position) const - { - const int layer = 0; - return mImpl->getTileAt(position.x(), position.y(), layer); - } }; using GuardedNavMeshCacheItem = Misc::ScopeGuarded; diff --git a/components/detournavigator/navmeshdata.hpp b/components/detournavigator/navmeshdata.hpp index 8ce79614b3..6e400af3bc 100644 --- a/components/detournavigator/navmeshdata.hpp +++ b/components/detournavigator/navmeshdata.hpp @@ -3,7 +3,6 @@ #include -#include #include namespace DetourNavigator @@ -21,7 +20,7 @@ namespace DetourNavigator struct NavMeshData { NavMeshDataValue mValue; - int mSize; + int mSize = 0; NavMeshData() = default; diff --git a/components/detournavigator/navmeshmanager.cpp b/components/detournavigator/navmeshmanager.cpp index c42fb5c4be..c378230845 100644 --- a/components/detournavigator/navmeshmanager.cpp +++ b/components/detournavigator/navmeshmanager.cpp @@ -47,16 +47,17 @@ namespace DetourNavigator , mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager) {} - bool NavMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool NavMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { + const btCollisionShape& collisionShape = shape.getShape(); if (!mRecastMeshManager.addObject(id, shape, transform, areaType)) return false; - addChangedTiles(shape, transform, ChangeType::add); + addChangedTiles(collisionShape, transform, ChangeType::add); return true; } - bool NavMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool NavMeshManager::updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { return mRecastMeshManager.updateObject(id, shape, transform, areaType, @@ -72,11 +73,11 @@ namespace DetourNavigator return true; } - bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform) + bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift) { - if (!mRecastMeshManager.addWater(cellPosition, cellSize, transform)) + if (!mRecastMeshManager.addWater(cellPosition, cellSize, shift)) return false; - addChangedTiles(cellSize, transform, ChangeType::add); + addChangedTiles(cellSize, shift, ChangeType::add); return true; } @@ -85,7 +86,25 @@ namespace DetourNavigator const auto water = mRecastMeshManager.removeWater(cellPosition); if (!water) return false; - addChangedTiles(water->mCellSize, water->mTransform, ChangeType::remove); + addChangedTiles(water->mSize, water->mShift, ChangeType::remove); + return true; + } + + bool NavMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape) + { + if (!mRecastMeshManager.addHeightfield(cellPosition, cellSize, shift, shape)) + return false; + addChangedTiles(cellSize, shift, ChangeType::add); + return true; + } + + bool NavMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const auto heightfield = mRecastMeshManager.removeHeightfield(cellPosition); + if (!heightfield) + return false; + addChangedTiles(heightfield->mSize, heightfield->mShift, ChangeType::remove); return true; } @@ -231,13 +250,13 @@ namespace DetourNavigator [&] (const TilePosition& v) { addChangedTile(v, changeType); }); } - void NavMeshManager::addChangedTiles(const int cellSize, const btTransform& transform, + void NavMeshManager::addChangedTiles(const int cellSize, const osg::Vec3f& shift, const ChangeType changeType) { if (cellSize == std::numeric_limits::max()) return; - getTilesPositions(cellSize, transform, mSettings, + getTilesPositions(cellSize, shift, mSettings, [&] (const TilePosition& v) { addChangedTile(v, changeType); }); } diff --git a/components/detournavigator/navmeshmanager.hpp b/components/detournavigator/navmeshmanager.hpp index b176515e3b..76c9b1e0b9 100644 --- a/components/detournavigator/navmeshmanager.hpp +++ b/components/detournavigator/navmeshmanager.hpp @@ -6,6 +6,7 @@ #include "offmeshconnectionsmanager.hpp" #include "recastmeshtiles.hpp" #include "waitconditiontype.hpp" +#include "heightfieldshape.hpp" #include @@ -23,20 +24,25 @@ namespace DetourNavigator public: NavMeshManager(const Settings& settings); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool removeObject(const ObjectId id); void addAgent(const osg::Vec3f& agentHalfExtents); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); bool removeWater(const osg::Vec2i& cellPosition); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape); + + bool removeHeightfield(const osg::Vec2i& cellPosition); + bool reset(const osg::Vec3f& agentHalfExtents); void addOffMeshConnection(const ObjectId id, const osg::Vec3f& start, const osg::Vec3f& end, const AreaType areaType); @@ -68,7 +74,7 @@ namespace DetourNavigator void addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType); - void addChangedTiles(const int cellSize, const btTransform& transform, const ChangeType changeType); + void addChangedTiles(const int cellSize, const osg::Vec3f& shift, const ChangeType changeType); void addChangedTile(const TilePosition& tilePosition, const ChangeType changeType); diff --git a/components/detournavigator/navmeshtilescache.cpp b/components/detournavigator/navmeshtilescache.cpp index ccc41a666d..3d595f13a8 100644 --- a/components/detournavigator/navmeshtilescache.cpp +++ b/components/detournavigator/navmeshtilescache.cpp @@ -6,32 +6,18 @@ namespace DetourNavigator { - namespace - { - inline std::size_t getSize(const RecastMesh& recastMesh, - const std::vector& offMeshConnections) - { - const std::size_t indicesSize = recastMesh.getIndices().size() * sizeof(int); - const std::size_t verticesSize = recastMesh.getVertices().size() * sizeof(float); - const std::size_t areaTypesSize = recastMesh.getAreaTypes().size() * sizeof(AreaType); - const std::size_t waterSize = recastMesh.getWater().size() * sizeof(RecastMesh::Water); - const std::size_t offMeshConnectionsSize = offMeshConnections.size() * sizeof(OffMeshConnection); - return indicesSize + verticesSize + areaTypesSize + waterSize + offMeshConnectionsSize; - } - } - NavMeshTilesCache::NavMeshTilesCache(const std::size_t maxNavMeshDataSize) : mMaxNavMeshDataSize(maxNavMeshDataSize), mUsedNavMeshDataSize(0), mFreeNavMeshDataSize(0), mHitCount(0), mGetCount(0) {} NavMeshTilesCache::Value NavMeshTilesCache::get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections) + const RecastMesh& recastMesh) { const std::lock_guard lock(mMutex); ++mGetCount; - const auto tile = mValues.find(std::make_tuple(agentHalfExtents, changedTile, NavMeshKeyView(recastMesh, offMeshConnections))); + const auto tile = mValues.find(std::make_tuple(agentHalfExtents, changedTile, recastMesh)); if (tile == mValues.end()) return Value(); @@ -43,10 +29,10 @@ namespace DetourNavigator } NavMeshTilesCache::Value NavMeshTilesCache::set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections, - NavMeshData&& value) + const RecastMesh& recastMesh, std::unique_ptr&& value) { - const auto itemSize = static_cast(value.mSize) + getSize(recastMesh, offMeshConnections); + const auto itemSize = sizeof(RecastMesh) + getSize(recastMesh) + + (value == nullptr ? 0 : sizeof(PreparedNavMeshData) + getSize(*value)); const std::lock_guard lock(mMutex); @@ -56,13 +42,11 @@ namespace DetourNavigator while (!mFreeItems.empty() && mUsedNavMeshDataSize + itemSize > mMaxNavMeshDataSize) removeLeastRecentlyUsed(); - NavMeshKey navMeshKey { - RecastMeshData {recastMesh.getIndices(), recastMesh.getVertices(), recastMesh.getAreaTypes(), recastMesh.getWater()}, - offMeshConnections - }; + RecastMeshData key {recastMesh.getMesh(), recastMesh.getWater(), + recastMesh.getHeightfields(), recastMesh.getFlatHeightfields()}; - const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, std::move(navMeshKey), itemSize); - const auto emplaced = mValues.emplace(std::make_tuple(agentHalfExtents, changedTile, NavMeshKeyRef(iterator->mNavMeshKey)), iterator); + const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, std::move(key), itemSize); + const auto emplaced = mValues.emplace(std::make_tuple(agentHalfExtents, changedTile, std::cref(iterator->mRecastMeshData)), iterator); if (!emplaced.second) { @@ -73,7 +57,7 @@ namespace DetourNavigator return Value(*this, emplaced.first->second); } - iterator->mNavMeshData = std::move(value); + iterator->mPreparedNavMeshData = std::move(value); ++iterator->mUseCount; mUsedNavMeshDataSize += itemSize; mBusyItems.splice(mBusyItems.end(), mFreeItems, iterator); @@ -101,14 +85,15 @@ namespace DetourNavigator out.setAttribute(frameNumber, "NavMesh CacheSize", stats.mNavMeshCacheSize); out.setAttribute(frameNumber, "NavMesh UsedTiles", stats.mUsedNavMeshTiles); out.setAttribute(frameNumber, "NavMesh CachedTiles", stats.mCachedNavMeshTiles); - out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); + if (stats.mGetCount > 0) + out.setAttribute(frameNumber, "NavMesh CacheHitRate", static_cast(stats.mHitCount) / stats.mGetCount * 100.0); } void NavMeshTilesCache::removeLeastRecentlyUsed() { const auto& item = mFreeItems.back(); - const auto value = mValues.find(std::make_tuple(item.mAgentHalfExtents, item.mChangedTile, NavMeshKeyRef(item.mNavMeshKey))); + const auto value = mValues.find(std::make_tuple(item.mAgentHalfExtents, item.mChangedTile, std::cref(item.mRecastMeshData))); if (value == mValues.end()) return; diff --git a/components/detournavigator/navmeshtilescache.hpp b/components/detournavigator/navmeshtilescache.hpp index afa53beba0..06d6c69e9b 100644 --- a/components/detournavigator/navmeshtilescache.hpp +++ b/components/detournavigator/navmeshtilescache.hpp @@ -1,8 +1,7 @@ #ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H #define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H -#include "offmeshconnection.hpp" -#include "navmeshdata.hpp" +#include "preparednavmeshdata.hpp" #include "recastmesh.hpp" #include "tileposition.hpp" @@ -21,101 +20,30 @@ namespace osg namespace DetourNavigator { - struct NavMeshDataRef - { - unsigned char* mValue; - int mSize; - }; - struct RecastMeshData { - std::vector mIndices; - std::vector mVertices; - std::vector mAreaTypes; - std::vector mWater; + Mesh mMesh; + std::vector mWater; + std::vector mHeightfields; + std::vector mFlatHeightfields; }; inline bool operator <(const RecastMeshData& lhs, const RecastMeshData& rhs) { - return std::tie(lhs.mIndices, lhs.mVertices, lhs.mAreaTypes, lhs.mWater) - < std::tie(rhs.mIndices, rhs.mVertices, rhs.mAreaTypes, rhs.mWater); + return std::tie(lhs.mMesh, lhs.mWater, lhs.mHeightfields, lhs.mFlatHeightfields) + < std::tie(rhs.mMesh, rhs.mWater, rhs.mHeightfields, rhs.mFlatHeightfields); } inline bool operator <(const RecastMeshData& lhs, const RecastMesh& rhs) { - return std::tie(lhs.mIndices, lhs.mVertices, lhs.mAreaTypes, lhs.mWater) - < std::tie(rhs.getIndices(), rhs.getVertices(), rhs.getAreaTypes(), rhs.getWater()); + return std::tie(lhs.mMesh, lhs.mWater, lhs.mHeightfields, lhs.mFlatHeightfields) + < std::tie(rhs.getMesh(), rhs.getWater(), rhs.getHeightfields(), rhs.getFlatHeightfields()); } inline bool operator <(const RecastMesh& lhs, const RecastMeshData& rhs) { - return std::tie(lhs.getIndices(), lhs.getVertices(), lhs.getAreaTypes(), lhs.getWater()) - < std::tie(rhs.mIndices, rhs.mVertices, rhs.mAreaTypes, rhs.mWater); - } - - struct NavMeshKey - { - RecastMeshData mRecastMesh; - std::vector mOffMeshConnections; - }; - - inline bool operator <(const NavMeshKey& lhs, const NavMeshKey& rhs) - { - return std::tie(lhs.mRecastMesh, lhs.mOffMeshConnections) - < std::tie(rhs.mRecastMesh, rhs.mOffMeshConnections); - } - - struct NavMeshKeyRef - { - std::reference_wrapper mRef; - - explicit NavMeshKeyRef(const NavMeshKey& ref) : mRef(ref) {} - }; - - inline bool operator <(const NavMeshKeyRef& lhs, const NavMeshKeyRef& rhs) - { - return lhs.mRef.get() < rhs.mRef.get(); - } - - struct NavMeshKeyView - { - std::reference_wrapper mRecastMesh; - std::reference_wrapper> mOffMeshConnections; - - NavMeshKeyView(const RecastMesh& recastMesh, const std::vector& offMeshConnections) - : mRecastMesh(recastMesh), mOffMeshConnections(offMeshConnections) {} - }; - - inline bool operator <(const NavMeshKeyView& lhs, const NavMeshKey& rhs) - { - return std::tie(lhs.mRecastMesh.get(), lhs.mOffMeshConnections.get()) - < std::tie(rhs.mRecastMesh, rhs.mOffMeshConnections); - } - - inline bool operator <(const NavMeshKey& lhs, const NavMeshKeyView& rhs) - { - return std::tie(lhs.mRecastMesh, lhs.mOffMeshConnections) - < std::tie(rhs.mRecastMesh.get(), rhs.mOffMeshConnections.get()); - } - - template - inline bool operator <(const NavMeshKeyRef& lhs, const R& rhs) - { - return lhs.mRef.get() < rhs; - } - - template - inline bool operator <(const L& lhs, const NavMeshKeyRef& rhs) - { - return lhs < rhs.mRef.get(); - } - - template - inline bool operator <(const std::tuple& lhs, const std::tuple& rhs) - { - const auto left = std::tie(std::get<0>(lhs), std::get<1>(lhs)); - const auto right = std::tie(std::get<0>(rhs), std::get<1>(rhs)); - return std::tie(left, std::get<2>(lhs)) < std::tie(right, std::get<2>(rhs)); + return std::tie(lhs.getMesh(), lhs.getWater(), lhs.getHeightfields(), lhs.getFlatHeightfields()) + < std::tie(rhs.mMesh, rhs.mWater, rhs.mHeightfields, rhs.mFlatHeightfields); } class NavMeshTilesCache @@ -126,15 +54,16 @@ namespace DetourNavigator std::atomic mUseCount; osg::Vec3f mAgentHalfExtents; TilePosition mChangedTile; - NavMeshKey mNavMeshKey; - NavMeshData mNavMeshData; + RecastMeshData mRecastMeshData; + std::unique_ptr mPreparedNavMeshData; std::size_t mSize; - Item(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, NavMeshKey&& navMeshKey, std::size_t size) + Item(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, + RecastMeshData&& recastMeshData, std::size_t size) : mUseCount(0) , mAgentHalfExtents(agentHalfExtents) , mChangedTile(changedTile) - , mNavMeshKey(navMeshKey) + , mRecastMeshData(std::move(recastMeshData)) , mSize(size) {} }; @@ -181,9 +110,9 @@ namespace DetourNavigator return *this; } - NavMeshDataRef get() const + const PreparedNavMeshData& get() const { - return NavMeshDataRef {mIterator->mNavMeshData.mValue.get(), mIterator->mNavMeshData.mSize}; + return *mIterator->mPreparedNavMeshData; } operator bool() const @@ -208,11 +137,10 @@ namespace DetourNavigator NavMeshTilesCache(const std::size_t maxNavMeshDataSize); Value get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections); + const RecastMesh& recastMesh); Value set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, - const RecastMesh& recastMesh, const std::vector& offMeshConnections, - NavMeshData&& value); + const RecastMesh& recastMesh, std::unique_ptr&& value); Stats getStats() const; @@ -227,7 +155,7 @@ namespace DetourNavigator std::size_t mGetCount; std::list mBusyItems; std::list mFreeItems; - std::map, ItemIterator, std::less<>> mValues; + std::map>, ItemIterator, std::less<>> mValues; void removeLeastRecentlyUsed(); diff --git a/components/detournavigator/navmeshtileview.cpp b/components/detournavigator/navmeshtileview.cpp index 96f07c6b1c..336cd1ba84 100644 --- a/components/detournavigator/navmeshtileview.cpp +++ b/components/detournavigator/navmeshtileview.cpp @@ -1,4 +1,5 @@ #include "navmeshtileview.hpp" +#include "ref.hpp" #include #include @@ -10,47 +11,9 @@ namespace { - template - struct Ref - { - T& mRef; - - explicit Ref(T& ref) : mRef(ref) {} - - friend bool operator==(const Ref& lhs, const Ref& rhs) - { - return lhs.mRef == rhs.mRef; - } - }; - - template - struct ArrayRef - { - T (&mRef)[size]; - - explicit ArrayRef(T (&ref)[size]) : mRef(ref) {} - - friend bool operator==(const ArrayRef& lhs, const ArrayRef& rhs) - { - return std::equal(std::begin(lhs.mRef), std::end(lhs.mRef), std::begin(rhs.mRef)); - } - }; - - template - struct Span - { - T* mBegin; - T* mEnd; - - explicit Span(T* data, int size) : mBegin(data), mEnd(data + static_cast(size)) {} - - friend bool operator==(const Span& lhs, const Span& rhs) - { - // size is already equal if headers are equal - assert((lhs.mEnd - lhs.mBegin) == (rhs.mEnd - rhs.mBegin)); - return std::equal(lhs.mBegin, lhs.mEnd, rhs.mBegin); - } - }; + using DetourNavigator::ArrayRef; + using DetourNavigator::Ref; + using DetourNavigator::Span; auto makeTuple(const dtMeshHeader& v) { diff --git a/components/detournavigator/offmeshconnection.hpp b/components/detournavigator/offmeshconnection.hpp index 01bae02732..9312bc6c48 100644 --- a/components/detournavigator/offmeshconnection.hpp +++ b/components/detournavigator/offmeshconnection.hpp @@ -5,6 +5,7 @@ #include +#include #include namespace DetourNavigator diff --git a/components/detournavigator/preparednavmeshdata.cpp b/components/detournavigator/preparednavmeshdata.cpp new file mode 100644 index 0000000000..3fea46b26c --- /dev/null +++ b/components/detournavigator/preparednavmeshdata.cpp @@ -0,0 +1,52 @@ +#include "preparednavmeshdata.hpp" +#include "preparednavmeshdatatuple.hpp" + +#include +#include + +namespace +{ + using namespace DetourNavigator; + + void initPolyMeshDetail(rcPolyMeshDetail& value) noexcept + { + value.meshes = nullptr; + value.verts = nullptr; + value.tris = nullptr; + value.nmeshes = 0; + value.nverts = 0; + value.ntris = 0; + } + + void freePolyMeshDetail(rcPolyMeshDetail& value) noexcept + { + rcFree(value.meshes); + rcFree(value.verts); + rcFree(value.tris); + } +} + +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 + { + initPolyMeshDetail(mPolyMeshDetail); + } + + PreparedNavMeshData::~PreparedNavMeshData() noexcept + { + freePolyMeshDetail(mPolyMeshDetail); + } + + bool operator==(const PreparedNavMeshData& lhs, const PreparedNavMeshData& rhs) noexcept + { + return makeTuple(lhs) == makeTuple(rhs); + } +} diff --git a/components/detournavigator/preparednavmeshdata.hpp b/components/detournavigator/preparednavmeshdata.hpp new file mode 100644 index 0000000000..3566cfc71b --- /dev/null +++ b/components/detournavigator/preparednavmeshdata.hpp @@ -0,0 +1,50 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATA_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATA_H + +#include "recast.hpp" + +#include + +#include + +namespace DetourNavigator +{ + struct PreparedNavMeshData + { + unsigned int mUserId = 0; + float mCellSize = 0; + float mCellHeight = 0; + rcPolyMesh mPolyMesh; + rcPolyMeshDetail mPolyMeshDetail; + + PreparedNavMeshData() noexcept; + PreparedNavMeshData(const PreparedNavMeshData&) = delete; + + ~PreparedNavMeshData() noexcept; + + friend bool operator==(const PreparedNavMeshData& lhs, const PreparedNavMeshData& rhs) noexcept; + }; + + inline constexpr std::size_t getSize(const rcPolyMesh& value) noexcept + { + return getVertsLength(value) * sizeof(*value.verts) + + getPolysLength(value) * sizeof(*value.polys) + + getRegsLength(value) * sizeof(*value.regs) + + getFlagsLength(value) * sizeof(*value.flags) + + getAreasLength(value) * sizeof(*value.areas); + } + + inline constexpr std::size_t getSize(const rcPolyMeshDetail& value) noexcept + { + return getMeshesLength(value) * sizeof(*value.meshes) + + getVertsLength(value) * sizeof(*value.verts) + + getTrisLength(value) * sizeof(*value.tris); + } + + inline constexpr std::size_t getSize(const PreparedNavMeshData& value) noexcept + { + return getSize(value.mPolyMesh) + getSize(value.mPolyMeshDetail); + } +} + +#endif diff --git a/components/detournavigator/preparednavmeshdatatuple.hpp b/components/detournavigator/preparednavmeshdatatuple.hpp new file mode 100644 index 0000000000..8ff1267370 --- /dev/null +++ b/components/detournavigator/preparednavmeshdatatuple.hpp @@ -0,0 +1,52 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATATUPLE_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_PREPAREDNAVMESHDATATUPLE_H + +#include "preparednavmeshdata.hpp" +#include "ref.hpp" +#include "recast.hpp" + +#include + +#include + +namespace DetourNavigator +{ + constexpr auto makeTuple(const rcPolyMesh& v) noexcept + { + 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)), + ArrayRef(v.bmin), + ArrayRef(v.bmax), + v.cs, + v.ch, + v.borderSize, + v.maxEdgeError + ); + } + + constexpr auto makeTuple(const rcPolyMeshDetail& v) noexcept + { + return std::tuple( + Span(v.meshes, getMeshesLength(v)), + Span(v.verts, getVertsLength(v)), + Span(v.tris, getTrisLength(v)) + ); + } + + constexpr auto makeTuple(const PreparedNavMeshData& v) noexcept + { + return std::tuple( + v.mUserId, + v.mCellHeight, + v.mCellSize, + Ref(v.mPolyMesh), + Ref(v.mPolyMeshDetail) + ); + } +} + +#endif diff --git a/components/detournavigator/recast.hpp b/components/detournavigator/recast.hpp new file mode 100644 index 0000000000..4e9ab329b7 --- /dev/null +++ b/components/detournavigator/recast.hpp @@ -0,0 +1,51 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECAST_H + +#include + +#include + +namespace DetourNavigator +{ + constexpr std::size_t getVertsLength(const rcPolyMesh& value) noexcept + { + return 3 * static_cast(value.nverts); + } + + constexpr std::size_t getPolysLength(const rcPolyMesh& value) noexcept + { + return 2 * static_cast(value.maxpolys * value.nvp); + } + + constexpr std::size_t getRegsLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.maxpolys); + } + + constexpr std::size_t getFlagsLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.npolys); + } + + constexpr std::size_t getAreasLength(const rcPolyMesh& value) noexcept + { + return static_cast(value.maxpolys); + } + + constexpr std::size_t getMeshesLength(const rcPolyMeshDetail& value) noexcept + { + return 4 * static_cast(value.nmeshes); + } + + constexpr std::size_t getVertsLength(const rcPolyMeshDetail& value) noexcept + { + return 3 * static_cast(value.nverts); + } + + constexpr std::size_t getTrisLength(const rcPolyMeshDetail& value) noexcept + { + return 4 * static_cast(value.ntris); + } +} + +#endif diff --git a/components/detournavigator/recastmesh.cpp b/components/detournavigator/recastmesh.cpp index 00d6ae556a..e2dea0ad6e 100644 --- a/components/detournavigator/recastmesh.cpp +++ b/components/detournavigator/recastmesh.cpp @@ -5,23 +5,53 @@ namespace DetourNavigator { - RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, std::vector indices, std::vector vertices, - std::vector areaTypes, std::vector water) + Mesh::Mesh(std::vector&& indices, std::vector&& vertices, std::vector&& areaTypes) + { + if (indices.size() / 3 != areaTypes.size()) + throw InvalidArgument("Number of flags doesn't match number of triangles: triangles=" + + std::to_string(indices.size() / 3) + ", areaTypes=" + std::to_string(areaTypes.size())); + indices.shrink_to_fit(); + vertices.shrink_to_fit(); + areaTypes.shrink_to_fit(); + mIndices = std::move(indices); + mVertices = std::move(vertices); + mAreaTypes = std::move(areaTypes); + } + + RecastMesh::RecastMesh(std::size_t generation, std::size_t revision, Mesh mesh, std::vector water, + std::vector heightfields, std::vector flatHeightfields) : mGeneration(generation) , mRevision(revision) - , mIndices(std::move(indices)) - , mVertices(std::move(vertices)) - , mAreaTypes(std::move(areaTypes)) + , mMesh(std::move(mesh)) , mWater(std::move(water)) + , mHeightfields(std::move(heightfields)) + , mFlatHeightfields(std::move(flatHeightfields)) { - if (getTrianglesCount() != mAreaTypes.size()) - throw InvalidArgument("Number of flags doesn't match number of triangles: triangles=" - + std::to_string(getTrianglesCount()) + ", areaTypes=" + std::to_string(mAreaTypes.size())); - if (getVerticesCount()) - rcCalcBounds(mVertices.data(), static_cast(getVerticesCount()), mBounds.mMin.ptr(), mBounds.mMax.ptr()); - mIndices.shrink_to_fit(); - mVertices.shrink_to_fit(); - mAreaTypes.shrink_to_fit(); + 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 0e6bc49204..c8e160603b 100644 --- a/components/detournavigator/recastmesh.hpp +++ b/components/detournavigator/recastmesh.hpp @@ -3,29 +3,93 @@ #include "areatype.hpp" #include "bounds.hpp" +#include "tilebounds.hpp" #include +#include + #include #include #include #include - -#include +#include namespace DetourNavigator { + class Mesh + { + public: + Mesh(std::vector&& indices, std::vector&& vertices, std::vector&& areaTypes); + + const std::vector& getIndices() const noexcept { return mIndices; } + const std::vector& getVertices() const noexcept { return mVertices; } + const std::vector& getAreaTypes() const noexcept { return mAreaTypes; } + std::size_t getVerticesCount() const noexcept { return mVertices.size() / 3; } + std::size_t getTrianglesCount() const noexcept { return mAreaTypes.size(); } + + private: + std::vector mIndices; + std::vector mVertices; + std::vector mAreaTypes; + + friend inline bool operator<(const Mesh& lhs, const Mesh& rhs) noexcept + { + return std::tie(lhs.mIndices, lhs.mVertices, lhs.mAreaTypes) + < std::tie(rhs.mIndices, rhs.mVertices, rhs.mAreaTypes); + } + + friend inline std::size_t getSize(const Mesh& value) noexcept + { + return value.mIndices.size() * sizeof(int) + + value.mVertices.size() * sizeof(float) + + value.mAreaTypes.size() * sizeof(AreaType); + } + }; + + struct Cell + { + int mSize; + osg::Vec3f mShift; + }; + + struct Heightfield + { + TileBounds mBounds; + std::uint8_t mLength; + float mMinHeight; + float mMaxHeight; + osg::Vec3f mShift; + float mScale; + std::vector mHeights; + }; + + inline auto makeTuple(const Heightfield& v) noexcept + { + return std::tie(v.mBounds, v.mLength, v.mMinHeight, v.mMaxHeight, v.mShift, v.mScale, v.mHeights); + } + + inline bool operator<(const Heightfield& lhs, const Heightfield& rhs) noexcept + { + return makeTuple(lhs) < makeTuple(rhs); + } + + struct FlatHeightfield + { + TileBounds mBounds; + 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); + } + class RecastMesh { public: - struct Water - { - int mCellSize; - btTransform mTransform; - }; - - RecastMesh(std::size_t generation, std::size_t revision, std::vector indices, std::vector vertices, - std::vector areaTypes, 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 { @@ -37,34 +101,21 @@ namespace DetourNavigator return mRevision; } - const std::vector& getIndices() const - { - return mIndices; - } + const Mesh& getMesh() const noexcept { return mMesh; } - const std::vector& getVertices() const - { - return mVertices; - } - - const std::vector& getAreaTypes() const - { - return mAreaTypes; - } - - const std::vector& getWater() const + const std::vector& getWater() const { return mWater; } - std::size_t getVerticesCount() const + const std::vector& getHeightfields() const noexcept { - return mVertices.size() / 3; + return mHeightfields; } - std::size_t getTrianglesCount() const + const std::vector& getFlatHeightfields() const noexcept { - return mIndices.size() / 3; + return mFlatHeightfields; } const Bounds& getBounds() const @@ -75,22 +126,30 @@ namespace DetourNavigator private: std::size_t mGeneration; std::size_t mRevision; - std::vector mIndices; - std::vector mVertices; - std::vector mAreaTypes; - std::vector mWater; + Mesh mMesh; + 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) + + 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 RecastMesh::Water& lhs, const RecastMesh::Water& rhs) + inline bool operator<(const Cell& lhs, const Cell& rhs) noexcept { - return std::tie(lhs.mCellSize, lhs.mTransform) < std::tie(rhs.mCellSize, rhs.mTransform); - } - - inline bool operator <(const RecastMesh& lhs, const RecastMesh& rhs) - { - return std::tie(lhs.getIndices(), lhs.getVertices(), lhs.getAreaTypes(), lhs.getWater()) - < std::tie(rhs.getIndices(), rhs.getVertices(), rhs.getAreaTypes(), rhs.getWater()); + return std::tie(lhs.mSize, lhs.mShift) < std::tie(rhs.mSize, rhs.mShift); } } diff --git a/components/detournavigator/recastmeshbuilder.cpp b/components/detournavigator/recastmeshbuilder.cpp index 8ecbf1ec8e..73b731c247 100644 --- a/components/detournavigator/recastmeshbuilder.cpp +++ b/components/detournavigator/recastmeshbuilder.cpp @@ -1,12 +1,11 @@ #include "recastmeshbuilder.hpp" #include "debug.hpp" -#include "settings.hpp" -#include "settingsutils.hpp" #include "exceptions.hpp" #include #include #include +#include #include #include @@ -17,8 +16,8 @@ #include #include -#include #include +#include namespace DetourNavigator { @@ -26,48 +25,105 @@ namespace DetourNavigator namespace { - void optimizeRecastMesh(std::vector& indices, std::vector& vertices) + RecastMeshTriangle makeRecastMeshTriangle(const btVector3* vertices, const AreaType areaType) { - std::vector> uniqueVertices; - uniqueVertices.reserve(vertices.size() / 3); + RecastMeshTriangle result; + result.mAreaType = areaType; + for (std::size_t i = 0; i < 3; ++i) + result.mVertices[i] = Misc::Convert::makeOsgVec3f(vertices[i]); + return result; + } - for (std::size_t i = 0, n = vertices.size() / 3; i < n; ++i) - uniqueVertices.emplace_back(vertices[i * 3], vertices[i * 3 + 1], vertices[i * 3 + 2]); - - std::sort(uniqueVertices.begin(), uniqueVertices.end()); - const auto end = std::unique(uniqueVertices.begin(), uniqueVertices.end()); - uniqueVertices.erase(end, uniqueVertices.end()); - - if (uniqueVertices.size() == vertices.size() / 3) - return; - - for (std::size_t i = 0, n = indices.size(); i < n; ++i) - { - const auto index = indices[i]; - const auto vertex = std::make_tuple(vertices[index * 3], vertices[index * 3 + 1], vertices[index * 3 + 2]); - const auto it = std::lower_bound(uniqueVertices.begin(), uniqueVertices.end(), vertex); - assert(it != uniqueVertices.end()); - assert(*it == vertex); - indices[i] = std::distance(uniqueVertices.begin(), it); - } - - vertices.resize(uniqueVertices.size() * 3); - - for (std::size_t i = 0, n = uniqueVertices.size(); i < n; ++i) - { - vertices[i * 3] = std::get<0>(uniqueVertices[i]); - vertices[i * 3 + 1] = std::get<1>(uniqueVertices[i]); - vertices[i * 3 + 2] = std::get<2>(uniqueVertices[i]); - } + TileBounds maxCellTileBounds(int size, const osg::Vec3f& shift) + { + 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) + }; } } - RecastMeshBuilder::RecastMeshBuilder(const Settings& settings, const TileBounds& bounds) - : mSettings(settings) - , mBounds(bounds) + Mesh makeMesh(std::vector&& triangles, const osg::Vec3f& shift) + { + std::vector uniqueVertices; + uniqueVertices.reserve(3 * triangles.size()); + + for (const RecastMeshTriangle& triangle : triangles) + for (const osg::Vec3f& vertex : triangle.mVertices) + uniqueVertices.push_back(vertex); + + std::sort(uniqueVertices.begin(), uniqueVertices.end()); + uniqueVertices.erase(std::unique(uniqueVertices.begin(), uniqueVertices.end()), uniqueVertices.end()); + + std::vector indices; + indices.reserve(3 * triangles.size()); + std::vector areaTypes; + areaTypes.reserve(triangles.size()); + + for (const RecastMeshTriangle& triangle : triangles) + { + areaTypes.push_back(triangle.mAreaType); + + for (const osg::Vec3f& vertex : triangle.mVertices) + { + const auto it = std::lower_bound(uniqueVertices.begin(), uniqueVertices.end(), vertex); + assert(it != uniqueVertices.end()); + assert(*it == vertex); + indices.push_back(static_cast(it - uniqueVertices.begin())); + } + } + + triangles.clear(); + + std::vector vertices; + vertices.reserve(3 * uniqueVertices.size()); + + for (const osg::Vec3f& vertex : uniqueVertices) + { + vertices.push_back(vertex.x() + shift.x()); + vertices.push_back(vertex.y() + shift.y()); + vertices.push_back(vertex.z() + shift.z()); + } + + return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes)); + } + + Mesh makeMesh(const Heightfield& heightfield) + { + using BulletHelpers::makeProcessTriangleCallback; + using Misc::Convert::toOsg; + + constexpr int upAxis = 2; + constexpr bool flipQuadEdges = false; +#if BT_BULLET_VERSION < 310 + std::vector heights(heightfield.mHeights.begin(), heightfield.mHeights.end()); + btHeightfieldTerrainShape shape(static_cast(heightfield.mHeights.size() / heightfield.mLength), + static_cast(heightfield.mLength), heights.data(), 1, + heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, PHY_FLOAT, flipQuadEdges + ); +#else + btHeightfieldTerrainShape shape(static_cast(heightfield.mHeights.size() / heightfield.mLength), + static_cast(heightfield.mLength), heightfield.mHeights.data(), + heightfield.mMinHeight, heightfield.mMaxHeight, upAxis, flipQuadEdges); +#endif + shape.setLocalScaling(btVector3(heightfield.mScale, heightfield.mScale, 1)); + btVector3 aabbMin; + btVector3 aabbMax; + shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax); + std::vector triangles; + auto callback = makeProcessTriangleCallback([&] (btVector3* vertices, int, int) + { + triangles.emplace_back(makeRecastMeshTriangle(vertices, AreaType_ground)); + }); + shape.processAllTriangles(&callback, aabbMin, aabbMax); + const osg::Vec2f 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)); + } + + RecastMeshBuilder::RecastMeshBuilder(const TileBounds& bounds) noexcept + : mBounds(bounds) { - mBounds.mMin /= mSettings.get().mRecastScaleFactor; - mBounds.mMax /= mSettings.get().mRecastScaleFactor; } void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform, @@ -96,37 +152,26 @@ namespace DetourNavigator void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform, const AreaType areaType) { - return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int) + return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* vertices, int, int) { - for (std::size_t i = 3; i > 0; --i) - addTriangleVertex(triangle[i - 1]); - mAreaTypes.push_back(areaType); + RecastMeshTriangle triangle = makeRecastMeshTriangle(vertices, areaType); + std::reverse(triangle.mVertices.begin(), triangle.mVertices.end()); + mTriangles.emplace_back(triangle); })); } void RecastMeshBuilder::addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform, const AreaType areaType) { - return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int) + addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* vertices, int, int) { - for (std::size_t i = 0; i < 3; ++i) - addTriangleVertex(triangle[i]); - mAreaTypes.push_back(areaType); + mTriangles.emplace_back(makeRecastMeshTriangle(vertices, areaType)); })); } void RecastMeshBuilder::addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType) { - const auto indexOffset = static_cast(mVertices.size() / 3); - - for (int vertex = 0, count = shape.getNumVertices(); vertex < count; ++vertex) - { - btVector3 position; - shape.getVertex(vertex, position); - addVertex(transform(position)); - } - - const std::array indices {{ + constexpr std::array indices {{ 0, 2, 3, 3, 1, 0, 0, 4, 6, @@ -141,22 +186,72 @@ namespace DetourNavigator 4, 5, 7, }}; - std::transform(indices.begin(), indices.end(), std::back_inserter(mIndices), - [&] (int index) { return index + indexOffset; }); - - std::generate_n(std::back_inserter(mAreaTypes), 12, [=] { return areaType; }); + for (std::size_t i = 0; i < indices.size(); i += 3) + { + std::array vertices; + for (std::size_t j = 0; j < 3; ++j) + { + btVector3 position; + shape.getVertex(indices[i + j], position); + vertices[j] = transform(position); + } + mTriangles.emplace_back(makeRecastMeshTriangle(vertices.data(), areaType)); + } } - void RecastMeshBuilder::addWater(const int cellSize, const btTransform& transform) + void RecastMeshBuilder::addWater(const int cellSize, const osg::Vec3f& shift) { - mWater.push_back(RecastMesh::Water {cellSize, transform}); + mWater.push_back(Cell {cellSize, shift}); + } + + void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, float height) + { + if (const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift))) + mFlatHeightfields.emplace_back(FlatHeightfield {*intersection, height + shift.z()}); + } + + void RecastMeshBuilder::addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights, + std::size_t size, float minHeight, float maxHeight) + { + const auto intersection = getIntersection(mBounds, maxCellTileBounds(cellSize, shift)); + if (!intersection.has_value()) + return; + const float stepSize = static_cast(cellSize) / (size - 1); + const int halfCellSize = cellSize / 2; + const auto local = [&] (float v, float shift) { return (v - shift + halfCellSize) / stepSize; }; + const auto index = [&] (float v, int add) { return std::clamp(static_cast(v) + add, 0, size); }; + const std::size_t minX = index(std::round(local(intersection->mMin.x(), shift.x())), -1); + const std::size_t minY = index(std::round(local(intersection->mMin.y(), shift.y())), -1); + const std::size_t maxX = index(std::round(local(intersection->mMax.x(), shift.x())), 1); + const std::size_t maxY = index(std::round(local(intersection->mMax.y(), shift.y())), 1); + const std::size_t endX = std::min(maxX + 1, size); + const std::size_t endY = std::min(maxY + 1, size); + const std::size_t sliceSize = (endX - minX) * (endY - minY); + if (sliceSize == 0) + return; + std::vector tileHeights; + tileHeights.reserve(sliceSize); + for (std::size_t y = minY; y < endY; ++y) + for (std::size_t x = minX; x < endX; ++x) + tileHeights.push_back(heights[x + y * size]); + Heightfield heightfield; + heightfield.mBounds = *intersection; + 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); } std::shared_ptr RecastMeshBuilder::create(std::size_t generation, std::size_t revision) && { - optimizeRecastMesh(mIndices, mVertices); + std::sort(mTriangles.begin(), mTriangles.end()); std::sort(mWater.begin(), mWater.end()); - return std::make_shared(generation, revision, std::move(mIndices), std::move(mVertices), std::move(mAreaTypes), std::move(mWater)); + Mesh mesh = makeMesh(std::move(mTriangles)); + return std::make_shared(generation, revision, std::move(mesh), std::move(mWater), + std::move(mHeightfields), std::move(mFlatHeightfields)); } void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform, @@ -218,18 +313,4 @@ namespace DetourNavigator shape.processAllTriangles(&wrapper, aabbMin, aabbMax); } - - void RecastMeshBuilder::addTriangleVertex(const btVector3& worldPosition) - { - mIndices.push_back(static_cast(mVertices.size() / 3)); - addVertex(worldPosition); - } - - void RecastMeshBuilder::addVertex(const btVector3& worldPosition) - { - const auto navMeshPosition = toNavMeshCoordinates(mSettings, Misc::Convert::makeOsgVec3f(worldPosition)); - mVertices.push_back(navMeshPosition.x()); - mVertices.push_back(navMeshPosition.y()); - mVertices.push_back(navMeshPosition.z()); - } } diff --git a/components/detournavigator/recastmeshbuilder.hpp b/components/detournavigator/recastmeshbuilder.hpp index cb1b79377a..120e4b045a 100644 --- a/components/detournavigator/recastmeshbuilder.hpp +++ b/components/detournavigator/recastmeshbuilder.hpp @@ -4,8 +4,16 @@ #include "recastmesh.hpp" #include "tilebounds.hpp" +#include + #include +#include +#include +#include +#include +#include + class btBoxShape; class btCollisionShape; class btCompoundShape; @@ -15,12 +23,21 @@ class btTriangleCallback; namespace DetourNavigator { - struct Settings; + struct RecastMeshTriangle + { + AreaType mAreaType; + std::array mVertices; + + friend inline bool operator<(const RecastMeshTriangle& lhs, const RecastMeshTriangle& rhs) + { + return std::tie(lhs.mAreaType, lhs.mVertices) < std::tie(rhs.mAreaType, rhs.mVertices); + } + }; class RecastMeshBuilder { public: - RecastMeshBuilder(const Settings& settings, const TileBounds& bounds); + explicit RecastMeshBuilder(const TileBounds& bounds) noexcept; void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); @@ -32,26 +49,30 @@ namespace DetourNavigator void addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType); - void addWater(const int mCellSize, const btTransform& transform); + void addWater(const int mCellSize, const osg::Vec3f& shift); + + void addHeightfield(int cellSize, const osg::Vec3f& shift, float height); + + void addHeightfield(int cellSize, const osg::Vec3f& shift, const float* heights, std::size_t size, + float minHeight, float maxHeight); std::shared_ptr create(std::size_t generation, std::size_t revision) &&; private: - std::reference_wrapper mSettings; - TileBounds mBounds; - std::vector mIndices; - std::vector mVertices; - std::vector mAreaTypes; - std::vector mWater; + const TileBounds mBounds; + std::vector mTriangles; + std::vector mWater; + std::vector mHeightfields; + std::vector mFlatHeightfields; void addObject(const btConcaveShape& shape, const btTransform& transform, btTriangleCallback&& callback); void addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform, btTriangleCallback&& callback); - - void addTriangleVertex(const btVector3& worldPosition); - - void addVertex(const btVector3& worldPosition); }; + + Mesh makeMesh(std::vector&& triangles, const osg::Vec3f& shift = osg::Vec3f()); + + Mesh makeMesh(const Heightfield& heightfield); } #endif diff --git a/components/detournavigator/recastmeshmanager.cpp b/components/detournavigator/recastmeshmanager.cpp index e7afeb2848..2eeb90a9b5 100644 --- a/components/detournavigator/recastmeshmanager.cpp +++ b/components/detournavigator/recastmeshmanager.cpp @@ -1,5 +1,30 @@ #include "recastmeshmanager.hpp" #include "recastmeshbuilder.hpp" +#include "settings.hpp" +#include "heightfieldshape.hpp" + +#include + +#include + +namespace +{ + struct AddHeightfield + { + const DetourNavigator::Cell& mCell; + DetourNavigator::RecastMeshBuilder& mBuilder; + + void operator()(const DetourNavigator::HeightfieldSurface& v) + { + mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeights, v.mSize, v.mMinHeight, v.mMaxHeight); + } + + void operator()(DetourNavigator::HeightfieldPlane v) + { + mBuilder.addHeightfield(mCell.mSize, mCell.mShift, v.mHeight); + } + }; +} namespace DetourNavigator { @@ -10,27 +35,28 @@ namespace DetourNavigator { } - bool RecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool RecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { + const std::lock_guard lock(mMutex); const auto object = mObjects.lower_bound(id); if (object != mObjects.end() && object->first == id) return false; - const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), + mObjects.emplace_hint(object, id, OscillatingRecastMeshObject(RecastMeshObject(shape, transform, areaType), mRevision + 1)); - mObjects.emplace_hint(object, id, iterator); ++mRevision; return true; } bool RecastMeshManager::updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType) { + const std::lock_guard lock(mMutex); const auto object = mObjects.find(id); if (object == mObjects.end()) return false; const std::size_t lastChangeRevision = mLastNavMeshReportedChange.has_value() ? mLastNavMeshReportedChange->mRevision : mRevision; - if (!object->second->update(transform, areaType, lastChangeRevision, mTileBounds)) + if (!object->second.update(transform, areaType, lastChangeRevision, mTileBounds)) return false; ++mRevision; return true; @@ -38,63 +64,103 @@ namespace DetourNavigator std::optional RecastMeshManager::removeObject(const ObjectId id) { + const std::lock_guard lock(mMutex); const auto object = mObjects.find(id); if (object == mObjects.end()) return std::nullopt; - const RemovedRecastMeshObject result {object->second->getImpl().getShape(), object->second->getImpl().getTransform()}; - mObjectsOrder.erase(object->second); + const RemovedRecastMeshObject result {object->second.getImpl().getShape(), object->second.getImpl().getTransform()}; mObjects.erase(object); ++mRevision; return result; } - bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift) { - const auto iterator = mWaterOrder.emplace(mWaterOrder.end(), Water {cellSize, transform}); - if (!mWater.emplace(cellPosition, iterator).second) - { - mWaterOrder.erase(iterator); + const std::lock_guard lock(mMutex); + if (!mWater.emplace(cellPosition, Cell {cellSize, shift}).second) return false; - } ++mRevision; return true; } - std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional RecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { + const std::lock_guard lock(mMutex); const auto water = mWater.find(cellPosition); if (water == mWater.end()) return std::nullopt; ++mRevision; - const auto result = *water->second; - mWaterOrder.erase(water->second); + const Cell result = water->second; mWater.erase(water); return result; } - std::shared_ptr RecastMeshManager::getMesh() + bool RecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape) { - RecastMeshBuilder builder(mSettings, mTileBounds); - for (const auto& v : mWaterOrder) - builder.addWater(v.mCellSize, v.mTransform); - for (const auto& object : mObjectsOrder) + const std::lock_guard lock(mMutex); + if (!mHeightfields.emplace(cellPosition, Heightfield {Cell {cellSize, shift}, shape}).second) + return false; + ++mRevision; + return true; + } + + std::optional RecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) + { + const std::lock_guard lock(mMutex); + const auto it = mHeightfields.find(cellPosition); + if (it == mHeightfields.end()) + return std::nullopt; + ++mRevision; + const auto result = std::make_optional(it->second.mCell); + 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); + using Object = std::tuple< + osg::ref_ptr, + std::reference_wrapper, + btTransform, + AreaType + >; + std::vector objects; + std::size_t revision; { - const RecastMeshObject& v = object.getImpl(); - builder.addObject(v.getShape(), v.getTransform(), v.getAreaType()); + const std::lock_guard lock(mMutex); + for (const auto& [k, v] : mWater) + builder.addWater(v.mSize, v.mShift); + for (const auto& [cellPosition, v] : mHeightfields) + std::visit(AddHeightfield {v.mCell, builder}, v.mShape); + objects.reserve(mObjects.size()); + for (const auto& [k, object] : mObjects) + { + const RecastMeshObject& impl = object.getImpl(); + objects.emplace_back(impl.getHolder(), impl.getShape(), impl.getTransform(), impl.getAreaType()); + } + revision = mRevision; } - return std::move(builder).create(mGeneration, mRevision); + for (const auto& [holder, shape, transform, areaType] : objects) + builder.addObject(shape, transform, areaType); + return std::move(builder).create(mGeneration, revision); } bool RecastMeshManager::isEmpty() const { - return mObjects.empty(); + const std::lock_guard lock(mMutex); + return mObjects.empty() && mWater.empty() && mHeightfields.empty(); } void RecastMeshManager::reportNavMeshChange(const Version& recastMeshVersion, const Version& navMeshVersion) { if (recastMeshVersion.mGeneration != mGeneration) return; + const std::lock_guard lock(mMutex); if (mLastNavMeshReport.has_value() && navMeshVersion < mLastNavMeshReport->mNavMeshVersion) return; mLastNavMeshReport = {recastMeshVersion.mRevision, navMeshVersion}; @@ -105,6 +171,7 @@ namespace DetourNavigator Version RecastMeshManager::getVersion() const { + const std::lock_guard lock(mMutex); return Version {mGeneration, mRevision}; } } diff --git a/components/detournavigator/recastmeshmanager.hpp b/components/detournavigator/recastmeshmanager.hpp index 956170d3b4..e1c1567cb3 100644 --- a/components/detournavigator/recastmeshmanager.hpp +++ b/components/detournavigator/recastmeshmanager.hpp @@ -4,15 +4,19 @@ #include "oscillatingrecastmeshobject.hpp" #include "objectid.hpp" #include "version.hpp" +#include "recastmesh.hpp" +#include "heightfieldshape.hpp" #include #include -#include #include #include #include +#include +#include +#include class btCollisionShape; @@ -30,26 +34,25 @@ namespace DetourNavigator class RecastMeshManager { public: - struct Water - { - int mCellSize = 0; - btTransform mTransform; - }; - RecastMeshManager(const Settings& settings, const TileBounds& bounds, std::size_t generation); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); - - std::optional removeWater(const osg::Vec2i& cellPosition); - std::optional removeObject(const ObjectId id); - std::shared_ptr getMesh(); + bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); + + std::optional removeWater(const osg::Vec2i& cellPosition); + + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape); + + std::optional removeHeightfield(const osg::Vec2i& cellPosition); + + std::shared_ptr getMesh() const; bool isEmpty() const; @@ -64,14 +67,20 @@ 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::size_t mGeneration; - TileBounds mTileBounds; - std::list mObjectsOrder; - std::map::iterator> mObjects; - std::list mWaterOrder; - std::map::iterator> mWater; + std::map mObjects; + std::map mWater; + std::map mHeightfields; std::optional mLastNavMeshReportedChange; std::optional mLastNavMeshReport; }; diff --git a/components/detournavigator/recastmeshobject.cpp b/components/detournavigator/recastmeshobject.cpp index 3f35462781..8b4bc2fd6f 100644 --- a/components/detournavigator/recastmeshobject.cpp +++ b/components/detournavigator/recastmeshobject.cpp @@ -22,15 +22,36 @@ namespace DetourNavigator } return result; } + + std::vector makeChildrenObjects(const osg::ref_ptr& holder, + const btCompoundShape& shape, const AreaType areaType) + { + 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); + } + return result; + } + + std::vector makeChildrenObjects(const osg::ref_ptr& holder, + const btCollisionShape& shape, const AreaType areaType) + { + if (shape.isCompound()) + return makeChildrenObjects(holder, static_cast(shape), areaType); + return std::vector(); + } } - RecastMeshObject::RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, + RecastMeshObject::RecastMeshObject(const CollisionShape& shape, const btTransform& transform, const AreaType areaType) - : mShape(shape) + : mHolder(shape.getHolder()) + , mShape(shape.getShape()) , mTransform(transform) , mAreaType(areaType) - , mLocalScaling(shape.getLocalScaling()) - , mChildren(makeChildrenObjects(shape, mAreaType)) + , mLocalScaling(mShape.get().getLocalScaling()) + , mChildren(makeChildrenObjects(mHolder, mShape.get(), mAreaType)) { } @@ -57,20 +78,4 @@ namespace DetourNavigator || result; return result; } - - std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType) - { - if (shape.isCompound()) - return makeChildrenObjects(static_cast(shape), areaType); - else - return std::vector(); - } - - std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType) - { - std::vector result; - for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i) - result.emplace_back(*shape.getChildShape(i), shape.getChildTransform(i), areaType); - return result; - } } diff --git a/components/detournavigator/recastmeshobject.hpp b/components/detournavigator/recastmeshobject.hpp index e659300e68..0c50c2f346 100644 --- a/components/detournavigator/recastmeshobject.hpp +++ b/components/detournavigator/recastmeshobject.hpp @@ -5,6 +5,9 @@ #include +#include +#include + #include #include @@ -13,13 +16,34 @@ class btCompoundShape; namespace DetourNavigator { + class CollisionShape + { + public: + CollisionShape(osg::ref_ptr holder, const btCollisionShape& shape) + : mHolder(std::move(holder)) + , mShape(shape) + {} + + const osg::ref_ptr& getHolder() const { return mHolder; } + const btCollisionShape& getShape() const { return mShape; } + + private: + osg::ref_ptr mHolder; + std::reference_wrapper mShape; + }; + class RecastMeshObject { public: - RecastMeshObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType); + RecastMeshObject(const CollisionShape& 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; @@ -36,16 +60,13 @@ namespace DetourNavigator } private: + osg::ref_ptr mHolder; std::reference_wrapper mShape; btTransform mTransform; AreaType mAreaType; btVector3 mLocalScaling; std::vector mChildren; }; - - std::vector makeChildrenObjects(const btCollisionShape& shape, const AreaType areaType); - - std::vector makeChildrenObjects(const btCompoundShape& shape, const AreaType areaType); } #endif diff --git a/components/detournavigator/ref.hpp b/components/detournavigator/ref.hpp new file mode 100644 index 0000000000..5a9e49e22a --- /dev/null +++ b/components/detournavigator/ref.hpp @@ -0,0 +1,56 @@ +#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_REF_H +#define OPENMW_COMPONENTS_DETOURNAVIGATOR_REF_H + +#include +#include +#include + +namespace DetourNavigator +{ + template + struct Ref + { + T& mRef; + + constexpr explicit Ref(T& ref) noexcept : mRef(ref) {} + + friend bool operator==(const Ref& lhs, const Ref& rhs) + { + return lhs.mRef == rhs.mRef; + } + }; + + template + struct ArrayRef + { + T (&mRef)[size]; + + constexpr explicit ArrayRef(T (&ref)[size]) noexcept : mRef(ref) {} + + friend bool operator==(const ArrayRef& lhs, const ArrayRef& rhs) + { + return std::equal(std::begin(lhs.mRef), std::end(lhs.mRef), std::begin(rhs.mRef)); + } + }; + + template + struct Span + { + T* mBegin; + T* mEnd; + + constexpr explicit Span(T* data, int size) noexcept + : mBegin(data) + , mEnd(data + static_cast(size)) + {} + + friend bool operator==(const Span& lhs, const Span& rhs) + { + // size is already equal if headers are equal + assert((lhs.mEnd - lhs.mBegin) == (rhs.mEnd - rhs.mBegin)); + return std::equal(lhs.mBegin, lhs.mEnd, rhs.mBegin); + } + }; +} + +#endif diff --git a/components/detournavigator/settingsutils.hpp b/components/detournavigator/settingsutils.hpp index 245e4a7e56..258eb64c65 100644 --- a/components/detournavigator/settingsutils.hpp +++ b/components/detournavigator/settingsutils.hpp @@ -37,6 +37,11 @@ namespace DetourNavigator return value * settings.mRecastScaleFactor; } + inline osg::Vec2f toNavMeshCoordinates(const Settings& settings, osg::Vec2f position) + { + return position * settings.mRecastScaleFactor; + } + inline osg::Vec3f toNavMeshCoordinates(const Settings& settings, osg::Vec3f position) { std::swap(position.y(), position.z()); @@ -77,18 +82,9 @@ namespace DetourNavigator return static_cast(settings.mBorderSize) * settings.mCellSize; } - inline float getSwimLevel(const Settings& settings, const float agentHalfExtentsZ) + inline float getSwimLevel(const Settings& settings, const float waterLevel, const float agentHalfExtentsZ) { - return - settings.mSwimHeightScale * agentHalfExtentsZ; - } - - inline btTransform getSwimLevelTransform(const Settings& settings, const btTransform& transform, - const float agentHalfExtentsZ) - { - return btTransform( - transform.getBasis(), - transform.getOrigin() + btVector3(0, 0, getSwimLevel(settings, agentHalfExtentsZ) - agentHalfExtentsZ) - ); + return waterLevel - settings.mSwimHeightScale * agentHalfExtentsZ - agentHalfExtentsZ;; } inline float getRealTileSize(const Settings& settings) diff --git a/components/detournavigator/status.hpp b/components/detournavigator/status.hpp index 3715489acc..5f01f04758 100644 --- a/components/detournavigator/status.hpp +++ b/components/detournavigator/status.hpp @@ -6,6 +6,7 @@ namespace DetourNavigator enum class Status { Success, + PartialPath, NavMeshNotFound, StartPolygonNotFound, EndPolygonNotFound, @@ -21,6 +22,8 @@ namespace DetourNavigator { case Status::Success: return "success"; + case Status::PartialPath: + return "partial path is found"; case Status::NavMeshNotFound: return "navmesh is not found"; case Status::StartPolygonNotFound: diff --git a/components/detournavigator/tilebounds.hpp b/components/detournavigator/tilebounds.hpp index 83fe2b6296..8557045342 100644 --- a/components/detournavigator/tilebounds.hpp +++ b/components/detournavigator/tilebounds.hpp @@ -3,6 +3,10 @@ #include +#include +#include +#include + namespace DetourNavigator { struct TileBounds @@ -10,6 +14,24 @@ namespace DetourNavigator osg::Vec2f mMin; osg::Vec2f mMax; }; + + inline bool operator<(const TileBounds& lhs, const TileBounds& rhs) noexcept + { + return std::tie(lhs.mMin, lhs.mMax) < std::tie(rhs.mMin, rhs.mMax); + } + + inline std::optional getIntersection(const TileBounds& a, const TileBounds& b) noexcept + { + const float minX = std::max(a.mMin.x(), b.mMin.x()); + const float maxX = std::min(a.mMax.x(), b.mMax.x()); + if (minX > maxX) + return std::nullopt; + const float minY = std::max(a.mMin.y(), b.mMin.y()); + const float maxY = std::min(a.mMax.y(), b.mMax.y()); + if (minY > maxY) + return std::nullopt; + return TileBounds {osg::Vec2f(minX, minY), osg::Vec2f(maxX, maxY)}; + } } #endif diff --git a/components/detournavigator/tilecachedrecastmeshmanager.cpp b/components/detournavigator/tilecachedrecastmeshmanager.cpp index 42ae93e806..63d7e13f6b 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.cpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.cpp @@ -3,6 +3,8 @@ #include "gettilespositions.hpp" #include "settingsutils.hpp" +#include + #include #include @@ -12,14 +14,14 @@ namespace DetourNavigator : mSettings(settings) {} - bool TileCachedRecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, + bool TileCachedRecastMeshManager::addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType) { std::vector tilesPositions; const auto border = getBorderSize(mSettings); { auto tiles = mTiles.lock(); - getTilesPositions(shape, transform, mSettings, [&] (const TilePosition& tilePosition) + getTilesPositions(shape.getShape(), transform, mSettings, [&] (const TilePosition& tilePosition) { if (addTile(id, shape, transform, areaType, tilePosition, border, tiles.get())) tilesPositions.push_back(tilePosition); @@ -54,7 +56,7 @@ namespace DetourNavigator } bool TileCachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, - const btTransform& transform) + const osg::Vec3f& shift) { const auto border = getBorderSize(mSettings); @@ -67,7 +69,7 @@ namespace DetourNavigator const auto tiles = mTiles.lock(); for (auto& tile : *tiles) { - if (tile.second.addWater(cellPosition, cellSize, transform)) + if (tile.second->addWater(cellPosition, cellSize, shift)) { tilesPositions.push_back(tile.first); result = true; @@ -76,7 +78,7 @@ namespace DetourNavigator } else { - getTilesPositions(cellSize, transform, mSettings, [&] (const TilePosition& tilePosition) + getTilesPositions(cellSize, shift, mSettings, [&] (const TilePosition& tilePosition) { const auto tiles = mTiles.lock(); auto tile = tiles->find(tilePosition); @@ -86,9 +88,9 @@ namespace DetourNavigator tileBounds.mMin -= osg::Vec2f(border, border); tileBounds.mMax += osg::Vec2f(border, border); tile = tiles->insert(std::make_pair(tilePosition, - CachedRecastMeshManager(mSettings, tileBounds, mTilesGeneration))).first; + std::make_shared(mSettings, tileBounds, mTilesGeneration))).first; } - if (tile->second.addWater(cellPosition, cellSize, transform)) + if (tile->second->addWater(cellPosition, cellSize, shift)) { tilesPositions.push_back(tilePosition); result = true; @@ -102,20 +104,20 @@ namespace DetourNavigator return result; } - std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) + std::optional TileCachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition) { const auto object = mWaterTilesPositions.find(cellPosition); if (object == mWaterTilesPositions.end()) return std::nullopt; - std::optional result; + std::optional result; for (const auto& tilePosition : object->second) { const auto tiles = mTiles.lock(); const auto tile = tiles->find(tilePosition); if (tile == tiles->end()) continue; - const auto tileResult = tile->second.removeWater(cellPosition); - if (tile->second.isEmpty()) + const auto tileResult = tile->second->removeWater(cellPosition); + if (tile->second->isEmpty()) { tiles->erase(tile); ++mTilesGeneration; @@ -128,18 +130,79 @@ namespace DetourNavigator return result; } - std::shared_ptr TileCachedRecastMeshManager::getMesh(const TilePosition& tilePosition) + bool TileCachedRecastMeshManager::addHeightfield(const osg::Vec2i& cellPosition, int cellSize, + const osg::Vec3f& shift, const HeightfieldShape& shape) { - const auto tiles = mTiles.lock(); - const auto it = tiles->find(tilePosition); - if (it == tiles->end()) - return nullptr; - return it->second.getMesh(); + const auto border = getBorderSize(mSettings); + + auto& tilesPositions = mHeightfieldTilesPositions[cellPosition]; + + bool result = false; + + 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; + } + if (tile->second->addHeightfield(cellPosition, cellSize, shift, shape)) + { + tilesPositions.push_back(tilePosition); + result = true; + } + }); + + if (result) + ++mRevision; + + return result; } - bool TileCachedRecastMeshManager::hasTile(const TilePosition& tilePosition) + std::optional TileCachedRecastMeshManager::removeHeightfield(const osg::Vec2i& cellPosition) { - return mTiles.lockConst()->count(tilePosition); + const auto object = mHeightfieldTilesPositions.find(cellPosition); + if (object == mHeightfieldTilesPositions.end()) + return std::nullopt; + std::optional result; + for (const auto& tilePosition : object->second) + { + const auto tiles = mTiles.lock(); + const auto tile = tiles->find(tilePosition); + if (tile == tiles->end()) + continue; + const auto tileResult = tile->second->removeHeightfield(cellPosition); + if (tile->second->isEmpty()) + { + tiles->erase(tile); + ++mTilesGeneration; + } + if (tileResult && !result) + result = tileResult; + } + if (result) + ++mRevision; + return result; + } + + 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(); } std::size_t TileCachedRecastMeshManager::getRevision() const @@ -147,18 +210,18 @@ namespace DetourNavigator return mRevision; } - void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) + void TileCachedRecastMeshManager::reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) const { - const auto tiles = mTiles.lock(); + const auto tiles = mTiles.lockConst(); const auto it = tiles->find(tilePosition); if (it == tiles->end()) return; - it->second.reportNavMeshChange(recastMeshVersion, navMeshVersion); + it->second->reportNavMeshChange(recastMeshVersion, navMeshVersion); } - bool TileCachedRecastMeshManager::addTile(const ObjectId id, const btCollisionShape& shape, + bool TileCachedRecastMeshManager::addTile(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType, const TilePosition& tilePosition, float border, - std::map& tiles) + TilesMap& tiles) { auto tile = tiles.find(tilePosition); if (tile == tiles.end()) @@ -167,26 +230,26 @@ namespace DetourNavigator tileBounds.mMin -= osg::Vec2f(border, border); tileBounds.mMax += osg::Vec2f(border, border); tile = tiles.insert(std::make_pair( - tilePosition, CachedRecastMeshManager(mSettings, tileBounds, mTilesGeneration))).first; + tilePosition, std::make_shared(mSettings, tileBounds, mTilesGeneration))).first; } - return tile->second.addObject(id, shape, transform, areaType); + return tile->second->addObject(id, shape, transform, areaType); } bool TileCachedRecastMeshManager::updateTile(const ObjectId id, const btTransform& transform, - const AreaType areaType, const TilePosition& tilePosition, std::map& tiles) + const AreaType areaType, const TilePosition& tilePosition, TilesMap& tiles) { const auto tile = tiles.find(tilePosition); - return tile != tiles.end() && tile->second.updateObject(id, transform, areaType); + return tile != tiles.end() && tile->second->updateObject(id, transform, areaType); } std::optional TileCachedRecastMeshManager::removeTile(const ObjectId id, - const TilePosition& tilePosition, std::map& tiles) + const TilePosition& tilePosition, TilesMap& tiles) { const auto tile = tiles.find(tilePosition); if (tile == tiles.end()) return std::optional(); - const auto tileResult = tile->second.removeObject(id); - if (tile->second.isEmpty()) + auto tileResult = tile->second->removeObject(id); + if (tile->second->isEmpty()) { tiles.erase(tile); ++mTilesGeneration; diff --git a/components/detournavigator/tilecachedrecastmeshmanager.hpp b/components/detournavigator/tilecachedrecastmeshmanager.hpp index 23ecc77634..f6bc40d668 100644 --- a/components/detournavigator/tilecachedrecastmeshmanager.hpp +++ b/components/detournavigator/tilecachedrecastmeshmanager.hpp @@ -6,6 +6,7 @@ #include "settingsutils.hpp" #include "gettilespositions.hpp" #include "version.hpp" +#include "heightfieldshape.hpp" #include @@ -21,11 +22,11 @@ namespace DetourNavigator public: TileCachedRecastMeshManager(const Settings& settings); - bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool addObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType); template - bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, + bool updateObject(const ObjectId id, const CollisionShape& shape, const btTransform& transform, const AreaType areaType, OnChangedTile&& onChangedTile) { const auto object = mObjectsTilesPositions.find(id); @@ -55,7 +56,7 @@ namespace DetourNavigator changed = true; } }; - getTilesPositions(shape, transform, mSettings, onTilePosition); + getTilesPositions(shape.getShape(), transform, mSettings, onTilePosition); std::sort(newTiles.begin(), newTiles.end()); for (const auto& tile : currentTiles) { @@ -76,42 +77,47 @@ namespace DetourNavigator std::optional removeObject(const ObjectId id); - bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform); + bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const osg::Vec3f& shift); - std::optional removeWater(const osg::Vec2i& cellPosition); + std::optional removeWater(const osg::Vec2i& cellPosition); - std::shared_ptr getMesh(const TilePosition& tilePosition); + bool addHeightfield(const osg::Vec2i& cellPosition, int cellSize, const osg::Vec3f& shift, + const HeightfieldShape& shape); - bool hasTile(const TilePosition& tilePosition); + std::optional removeHeightfield(const osg::Vec2i& cellPosition); + + std::shared_ptr getMesh(const TilePosition& tilePosition) const; template - void forEachTile(Function&& function) + void forEachTile(Function&& function) const { - for (auto& [tilePosition, recastMeshManager] : *mTiles.lock()) - function(tilePosition, recastMeshManager); + for (auto& [tilePosition, recastMeshManager] : *mTiles.lockConst()) + function(tilePosition, *recastMeshManager); } std::size_t getRevision() const; - void reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion); + void reportNavMeshChange(const TilePosition& tilePosition, Version recastMeshVersion, Version navMeshVersion) const; private: + using TilesMap = std::map>; + const Settings& mSettings; - Misc::ScopeGuarded> mTiles; + Misc::ScopeGuarded mTiles; std::unordered_map> mObjectsTilesPositions; std::map> mWaterTilesPositions; + std::map> mHeightfieldTilesPositions; std::size_t mRevision = 0; std::size_t mTilesGeneration = 0; - bool addTile(const ObjectId id, const btCollisionShape& shape, const btTransform& transform, - const AreaType areaType, const TilePosition& tilePosition, float border, - std::map& tiles); + bool addTile(const ObjectId id, const CollisionShape& shape, const btTransform& transform, + const AreaType areaType, const TilePosition& tilePosition, float border, TilesMap& tiles); bool updateTile(const ObjectId id, const btTransform& transform, const AreaType areaType, - const TilePosition& tilePosition, std::map& tiles); + const TilePosition& tilePosition, TilesMap& tiles); std::optional removeTile(const ObjectId id, const TilePosition& tilePosition, - std::map& tiles); + TilesMap& tiles); }; } diff --git a/components/esm/activespells.cpp b/components/esm/activespells.cpp index 4017a4933e..22f862b6e4 100644 --- a/components/esm/activespells.cpp +++ b/components/esm/activespells.cpp @@ -3,44 +3,67 @@ #include "esmreader.hpp" #include "esmwriter.hpp" -namespace ESM +namespace { - - void ActiveSpells::save(ESMWriter &esm) const + void save(ESM::ESMWriter& esm, const std::vector& spells, const std::string& tag) { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) + for (const auto& params : spells) { - esm.writeHNString ("ID__", it->first); - - const ActiveSpellParams& params = it->second; + esm.writeHNString (tag, params.mId); esm.writeHNT ("CAST", params.mCasterActorId); esm.writeHNString ("DISP", params.mDisplayName); - - for (std::vector::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt) + esm.writeHNT ("TYPE", params.mType); + if(params.mItem.isSet()) + params.mItem.save(esm, true, "ITEM"); + if(params.mWorsenings >= 0) { - esm.writeHNT ("MGEF", effectIt->mEffectId); - if (effectIt->mArg != -1) - esm.writeHNT ("ARG_", effectIt->mArg); - esm.writeHNT ("MAGN", effectIt->mMagnitude); - esm.writeHNT ("DURA", effectIt->mDuration); - esm.writeHNT ("EIND", effectIt->mEffectIndex); - esm.writeHNT ("LEFT", effectIt->mTimeLeft); + esm.writeHNT ("WORS", params.mWorsenings); + esm.writeHNT ("TIME", params.mNextWorsening); + } + + for (auto effect : params.mEffects) + { + esm.writeHNT ("MGEF", effect.mEffectId); + if (effect.mArg != -1) + esm.writeHNT ("ARG_", effect.mArg); + esm.writeHNT ("MAGN", effect.mMagnitude); + esm.writeHNT ("MAGN", effect.mMinMagnitude); + esm.writeHNT ("MAGN", effect.mMaxMagnitude); + esm.writeHNT ("DURA", effect.mDuration); + esm.writeHNT ("EIND", effect.mEffectIndex); + esm.writeHNT ("LEFT", effect.mTimeLeft); + esm.writeHNT ("FLAG", effect.mFlags); } } } - void ActiveSpells::load(ESMReader &esm) + void load(ESM::ESMReader& esm, std::vector& spells, const char* tag) { int format = esm.getFormat(); - while (esm.isNextSub("ID__")) + while (esm.isNextSub(tag)) { - std::string spellId = esm.getHString(); - - ActiveSpellParams params; + ESM::ActiveSpells::ActiveSpellParams params; + params.mId = esm.getHString(); esm.getHNT (params.mCasterActorId, "CAST"); params.mDisplayName = esm.getHNString ("DISP"); + params.mItem.unset(); + if (format < 17) + params.mType = ESM::ActiveSpells::Type_Temporary; + else + { + esm.getHNT (params.mType, "TYPE"); + if(esm.peekNextSub("ITEM")) + params.mItem.load(esm, true, "ITEM"); + } + if(esm.isNextSub("WORS")) + { + esm.getHT(params.mWorsenings); + esm.getHNT(params.mNextWorsening, "TIME"); + } + else + params.mWorsenings = -1; // spell casting timestamp, no longer used if (esm.isNextSub("TIME")) @@ -48,11 +71,21 @@ namespace ESM while (esm.isNextSub("MGEF")) { - ActiveEffect effect; + ESM::ActiveEffect effect; esm.getHT(effect.mEffectId); effect.mArg = -1; esm.getHNOT(effect.mArg, "ARG_"); esm.getHNT (effect.mMagnitude, "MAGN"); + if (format < 17) + { + effect.mMinMagnitude = effect.mMagnitude; + effect.mMaxMagnitude = effect.mMagnitude; + } + else + { + esm.getHNT (effect.mMinMagnitude, "MAGN"); + esm.getHNT (effect.mMaxMagnitude, "MAGN"); + } esm.getHNT (effect.mDuration, "DURA"); effect.mEffectIndex = -1; esm.getHNOT (effect.mEffectIndex, "EIND"); @@ -60,10 +93,30 @@ namespace ESM effect.mTimeLeft = effect.mDuration; else esm.getHNT (effect.mTimeLeft, "LEFT"); + if (format < 17) + effect.mFlags = ESM::ActiveEffect::Flag_None; + else + esm.getHNT (effect.mFlags, "FLAG"); params.mEffects.push_back(effect); } - mSpells.insert(std::make_pair(spellId, params)); + spells.emplace_back(params); } } } + +namespace ESM +{ + + void ActiveSpells::save(ESMWriter &esm) const + { + ::save(esm, mSpells, "ID__"); + ::save(esm, mQueue, "QID_"); + } + + void ActiveSpells::load(ESMReader &esm) + { + ::load(esm, mSpells, "ID__"); + ::load(esm, mQueue, "QID_"); + } +} diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 1b7f8b319c..8b5f1f1946 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -1,11 +1,12 @@ #ifndef OPENMW_ESM_ACTIVESPELLS_H #define OPENMW_ESM_ACTIVESPELLS_H -#include "effectlist.hpp" +#include "cellref.hpp" #include "defs.hpp" +#include "effectlist.hpp" #include -#include +#include namespace ESM { @@ -14,29 +15,53 @@ namespace ESM // Parameters of an effect concerning lasting effects. // Note we are not using ENAMstruct since the magnitude may be modified by magic resistance, etc. - // It could also be a negative magnitude, in case of inversing an effect, e.g. Absorb spell causes damage on target, but heals the caster. struct ActiveEffect { + enum Flags + { + Flag_None = 0, + Flag_Applied = 1 << 0, + Flag_Remove = 1 << 1, + Flag_Ignore_Resistances = 1 << 2 + }; + int mEffectId; float mMagnitude; + float mMinMagnitude; + float mMaxMagnitude; int mArg; // skill or attribute float mDuration; float mTimeLeft; int mEffectIndex; + int mFlags; }; // format 0, saved games only struct ActiveSpells { + enum EffectType + { + Type_Temporary, + Type_Ability, + Type_Enchantment, + Type_Permanent, + Type_Consumable + }; + struct ActiveSpellParams { + std::string mId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; + RefNum mItem; + EffectType mType; + int mWorsenings; + TimeStamp mNextWorsening; }; - typedef std::multimap TContainer; - TContainer mSpells; + std::vector mSpells; + std::vector mQueue; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index cb383992c6..d5030a6580 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -110,16 +110,31 @@ void ESM::CreatureStats::load (ESMReader &esm) mAiSequence.load(esm); mMagicEffects.load(esm); - while (esm.isNextSub("SUMM")) + if (esm.getFormat() < 17) { - int magicEffect; - esm.getHT(magicEffect); - std::string source = esm.getHNOString("SOUR"); - int effectIndex = -1; - esm.getHNOT (effectIndex, "EIND"); - int actorId; - esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + std::string source = esm.getHNOString("SOUR"); + int effectIndex = -1; + esm.getHNOT (effectIndex, "EIND"); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; + mSummonedCreatures.emplace(magicEffect, actorId); + } + } + else + { + while (esm.isNextSub("SUMM")) + { + int magicEffect; + esm.getHT(magicEffect); + int actorId; + esm.getHNT (actorId, "ACID"); + mSummonedCreatures.emplace(magicEffect, actorId); + } } while (esm.isNextSub("GRAV")) @@ -214,14 +229,10 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSequence.save(esm); mMagicEffects.save(esm); - for (const auto& summon : mSummonedCreatureMap) + for (const auto& [effectId, actorId] : mSummonedCreatures) { - esm.writeHNT ("SUMM", summon.first.mEffectId); - esm.writeHNString ("SOUR", summon.first.mSourceId); - int effectIndex = summon.first.mEffectIndex; - if (effectIndex != -1) - esm.writeHNT ("EIND", effectIndex); - esm.writeHNT ("ACID", summon.second); + esm.writeHNT ("SUMM", effectId); + esm.writeHNT ("ACID", actorId); } for (int key : mSummonGraveyard) @@ -235,15 +246,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const for (int i=0; i<4; ++i) mAiSettings[i].save(esm); } - - for (const auto& corprusSpell : mCorprusSpells) - { - esm.writeHNString("CORP", corprusSpell.first); - - const CorprusStats & stats = corprusSpell.second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } } void ESM::CreatureStats::blank() diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 13bc50008c..651b126d0e 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -40,6 +40,7 @@ namespace ESM StatState mAiSettings[4]; std::map mSummonedCreatureMap; + std::multimap mSummonedCreatures; std::vector mSummonGraveyard; ESM::TimeStamp mTradeTime; diff --git a/components/esm/debugprofile.cpp b/components/esm/debugprofile.cpp index 1dcf661fc9..6276258c48 100644 --- a/components/esm/debugprofile.cpp +++ b/components/esm/debugprofile.cpp @@ -9,6 +9,7 @@ unsigned int ESM::DebugProfile::sRecordId = REC_DBGP; void ESM::DebugProfile::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { diff --git a/components/esm/debugprofile.hpp b/components/esm/debugprofile.hpp index c056750a88..8340404c23 100644 --- a/components/esm/debugprofile.hpp +++ b/components/esm/debugprofile.hpp @@ -19,6 +19,7 @@ namespace ESM Flag_Global = 4 // make available from main menu (i.e. not location specific) }; + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/esmreader.cpp b/components/esm/esmreader.cpp index a30ad2c069..dbf713315b 100644 --- a/components/esm/esmreader.cpp +++ b/components/esm/esmreader.cpp @@ -125,8 +125,7 @@ void ESMReader::getHExact(void*p, int size) { getSubHeader(); if (size != static_cast (mCtx.leftSub)) - fail("Size mismatch, requested " + std::to_string(size) + " but got " - + std::to_string(mCtx.leftSub)); + reportSubSizeMismatch(size, mCtx.leftSub); getExact(p, size); } @@ -198,7 +197,7 @@ void ESMReader::skipHSubSize(int size) { skipHSub(); if (static_cast (mCtx.leftSub) != size) - fail("skipHSubSize() mismatch"); + reportSubSizeMismatch(mCtx.leftSub, size); } void ESMReader::skipHSubUntil(const char *name) @@ -264,7 +263,7 @@ void ESMReader::getRecHeader(uint32_t &flags) // Check that sizes add up if (mCtx.leftFile < mCtx.leftRec) - fail("Record size is larger than rest of file"); + reportSubSizeMismatch(mCtx.leftFile, mCtx.leftRec); // Adjust number of bytes mCtx.left in file mCtx.leftFile -= mCtx.leftRec; diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index 6129147d33..a438dca0cd 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -134,11 +134,7 @@ public: { getSubHeader(); if (mCtx.leftSub != sizeof(X)) - { - fail("getHT(): subrecord size mismatch,requested " - + std::to_string(sizeof(X)) + ", got" - + std::to_string(mCtx.leftSub)); - } + reportSubSizeMismatch(sizeof(X), mCtx.leftSub); getT(x); } @@ -261,6 +257,13 @@ public: size_t getFileSize() const { return mFileSize; } private: + [[noreturn]] void reportSubSizeMismatch(size_t want, size_t got) { + fail("record size mismatch, requested " + + std::to_string(want) + + ", got" + + std::to_string(got)); + } + void clearCtx(); Files::IStreamPtr mEsm; diff --git a/components/esm/filter.cpp b/components/esm/filter.cpp index 5bae1ed03e..8d1b755055 100644 --- a/components/esm/filter.cpp +++ b/components/esm/filter.cpp @@ -9,6 +9,7 @@ unsigned int ESM::Filter::sRecordId = REC_FILT; void ESM::Filter::load (ESMReader& esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); while (esm.hasMoreSubs()) { diff --git a/components/esm/filter.hpp b/components/esm/filter.hpp index b1c511ebba..78d51cec00 100644 --- a/components/esm/filter.hpp +++ b/components/esm/filter.hpp @@ -12,6 +12,7 @@ namespace ESM { static unsigned int sRecordId; + unsigned int mRecordFlags; std::string mId; std::string mDescription; diff --git a/components/esm/loadbody.cpp b/components/esm/loadbody.cpp index 4ddefc92c7..239cff7c8b 100644 --- a/components/esm/loadbody.cpp +++ b/components/esm/loadbody.cpp @@ -11,6 +11,7 @@ namespace ESM void BodyPart::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadbody.hpp b/components/esm/loadbody.hpp index bf320330ff..1be775ffec 100644 --- a/components/esm/loadbody.hpp +++ b/components/esm/loadbody.hpp @@ -58,6 +58,7 @@ struct BodyPart }; BYDTstruct mData; + unsigned int mRecordFlags; std::string mId, mModel, mRace; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadbsgn.cpp b/components/esm/loadbsgn.cpp index 1f679af39a..7514f1f85b 100644 --- a/components/esm/loadbsgn.cpp +++ b/components/esm/loadbsgn.cpp @@ -11,6 +11,7 @@ namespace ESM void BirthSign::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); diff --git a/components/esm/loadbsgn.hpp b/components/esm/loadbsgn.hpp index 24d27a7f85..806323bf35 100644 --- a/components/esm/loadbsgn.hpp +++ b/components/esm/loadbsgn.hpp @@ -17,6 +17,7 @@ struct BirthSign /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "BirthSign"; } + unsigned int mRecordFlags; std::string mId, mName, mDescription, mTexture; // List of powers and abilities that come with this birth sign. diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index d43911135a..d2cb23146f 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -1,10 +1,12 @@ #include "loadcell.hpp" #include +#include #include #include +#include #include #include "esmreader.hpp" @@ -109,6 +111,7 @@ namespace ESM void Cell::loadCell(ESMReader &esm, bool saveContext) { + bool overriding = !mName.empty(); bool isLoaded = false; mHasAmbi = false; while (!isLoaded && esm.hasMoreSubs()) @@ -123,8 +126,17 @@ namespace ESM mWaterInt = true; break; case ESM::FourCC<'W','H','G','T'>::value: - esm.getHT(mWater); + float waterLevel; + esm.getHT(waterLevel); mWaterInt = false; + if(!std::isfinite(waterLevel)) + { + if(!overriding) + mWater = std::numeric_limits::max(); + Log(Debug::Warning) << "Warning: Encountered invalid water level in cell " << mName << " defined in " << esm.getContext().filename; + } + else + mWater = waterLevel; break; case ESM::FourCC<'A','M','B','I'>::value: esm.getHT(mAmbi); @@ -224,7 +236,7 @@ namespace ESM return region + ' ' + cellGrid; } - bool Cell::getNextRef(ESMReader &esm, CellRef &ref, bool &isDeleted, bool ignoreMoves, MovedCellRef *mref) + bool Cell::getNextRef(ESMReader& esm, CellRef& ref, bool& isDeleted) { isDeleted = false; @@ -232,22 +244,18 @@ namespace ESM if (!esm.hasMoreSubs()) return false; - // NOTE: We should not need this check. It is a safety check until we have checked - // more plugins, and how they treat these moved references. - if (esm.isNextSub("MVRF")) + // MVRF are FRMR are present in pairs. MVRF indicates that following FRMR describes moved CellRef. + // This function has to skip all moved CellRefs therefore read all such pairs to ignored values. + while (esm.isNextSub("MVRF")) { - if (ignoreMoves) - { - esm.getHT (mref->mRefNum.mIndex); - esm.getHNOT (mref->mTarget, "CNDT"); - adjustRefNum (mref->mRefNum, esm); - } - else - { - // skip rest of cell record (moved references), they are handled elsewhere - esm.skipRecord(); // skip MVRF, CNDT + MovedCellRef movedCellRef; + esm.getHT(movedCellRef.mRefNum.mIndex); + esm.getHNOT(movedCellRef.mTarget, "CNDT"); + CellRef skippedCellRef; + if (!esm.peekNextSub("FRMR")) return false; - } + bool skippedDeleted; + skippedCellRef.load(esm, skippedDeleted); } if (esm.peekNextSub("FRMR")) @@ -263,6 +271,29 @@ namespace ESM return false; } + bool Cell::getNextRef(ESMReader& esm, CellRef& cellRef, bool& deleted, MovedCellRef& movedCellRef, bool& moved) + { + deleted = false; + moved = false; + + if (!esm.hasMoreSubs()) + return false; + + if (esm.isNextSub("MVRF")) + { + moved = true; + getNextMVRF(esm, movedCellRef); + } + + if (!esm.peekNextSub("FRMR")) + return false; + + cellRef.load(esm, deleted); + adjustRefNum(cellRef.mRefNum, esm); + + return true; + } + bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref) { esm.getHT(mref.mRefNum.mIndex); @@ -280,7 +311,7 @@ namespace ESM mWater = 0; mWaterInt = false; mMapColor = 0; - mRefNumCounter = -1; + mRefNumCounter = 0; mData.mFlags = 0; mData.mX = 0; diff --git a/components/esm/loadcell.hpp b/components/esm/loadcell.hpp index c49dc20c59..18e929e13b 100644 --- a/components/esm/loadcell.hpp +++ b/components/esm/loadcell.hpp @@ -94,7 +94,7 @@ struct Cell mWater(0), mWaterInt(false), mMapColor(0), - mRefNumCounter(-1) + mRefNumCounter(0) {} // Interior cells are indexed by this (it's the 'id'), for exterior @@ -181,12 +181,9 @@ struct Cell All fields of the CellRef struct are overwritten. You can safely reuse one memory location without blanking it between calls. */ - /// \param ignoreMoves ignore MVRF record and read reference like a regular CellRef. - static bool getNextRef(ESMReader &esm, - CellRef &ref, - bool &isDeleted, - bool ignoreMoves = false, - MovedCellRef *mref = nullptr); + static bool getNextRef(ESMReader& esm, CellRef& ref, bool& deleted); + + static bool getNextRef(ESMReader& esm, CellRef& cellRef, bool& deleted, MovedCellRef& movedCellRef, bool& moved); /* This fetches an MVRF record, which is used to track moved references. * Since they are comparably rare, we use a separate method for this. diff --git a/components/esm/loadclas.cpp b/components/esm/loadclas.cpp index b76fc57067..7526fe4f52 100644 --- a/components/esm/loadclas.cpp +++ b/components/esm/loadclas.cpp @@ -41,6 +41,7 @@ namespace ESM void Class::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadclas.hpp b/components/esm/loadclas.hpp index 833dd6757d..1000879c4c 100644 --- a/components/esm/loadclas.hpp +++ b/components/esm/loadclas.hpp @@ -70,6 +70,7 @@ struct Class ///< Throws an exception for invalid values of \a index. }; // 60 bytes + unsigned int mRecordFlags; std::string mId, mName, mDescription; CLDTstruct mData; diff --git a/components/esm/loadench.cpp b/components/esm/loadench.cpp index 3c1a2f1eda..ed3de90b50 100644 --- a/components/esm/loadench.cpp +++ b/components/esm/loadench.cpp @@ -11,6 +11,7 @@ namespace ESM void Enchantment::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); bool hasName = false; diff --git a/components/esm/loadench.hpp b/components/esm/loadench.hpp index b98549ef35..a4e1e8362c 100644 --- a/components/esm/loadench.hpp +++ b/components/esm/loadench.hpp @@ -42,6 +42,7 @@ struct Enchantment int mFlags; }; + unsigned int mRecordFlags; std::string mId; ENDTstruct mData; EffectList mEffects; diff --git a/components/esm/loadfact.cpp b/components/esm/loadfact.cpp index bd0962721b..b71348de44 100644 --- a/components/esm/loadfact.cpp +++ b/components/esm/loadfact.cpp @@ -29,6 +29,7 @@ namespace ESM void Faction::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mReactions.clear(); for (int i=0;i<10;++i) diff --git a/components/esm/loadfact.hpp b/components/esm/loadfact.hpp index 098ed43096..6a42377901 100644 --- a/components/esm/loadfact.hpp +++ b/components/esm/loadfact.hpp @@ -34,6 +34,7 @@ struct Faction /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Faction"; } + unsigned int mRecordFlags; std::string mId, mName; struct FADTstruct diff --git a/components/esm/loadglob.cpp b/components/esm/loadglob.cpp index 72ecce503c..d2226d1738 100644 --- a/components/esm/loadglob.cpp +++ b/components/esm/loadglob.cpp @@ -11,6 +11,7 @@ namespace ESM void Global::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString ("NAME"); diff --git a/components/esm/loadglob.hpp b/components/esm/loadglob.hpp index 0533cc95ea..9dd58e6c67 100644 --- a/components/esm/loadglob.hpp +++ b/components/esm/loadglob.hpp @@ -21,6 +21,7 @@ struct Global /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Global"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadgmst.cpp b/components/esm/loadgmst.cpp index da8d256e7d..6d4ac1b202 100644 --- a/components/esm/loadgmst.cpp +++ b/components/esm/loadgmst.cpp @@ -11,6 +11,7 @@ namespace ESM void GameSetting::load (ESMReader &esm, bool &isDeleted) { isDeleted = false; // GameSetting record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); mId = esm.getHNString("NAME"); mValue.read (esm, ESM::Variant::Format_Gmst); diff --git a/components/esm/loadgmst.hpp b/components/esm/loadgmst.hpp index c40d348fe4..931ee286a4 100644 --- a/components/esm/loadgmst.hpp +++ b/components/esm/loadgmst.hpp @@ -22,6 +22,7 @@ struct GameSetting /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "GameSetting"; } + unsigned int mRecordFlags; std::string mId; Variant mValue; diff --git a/components/esm/loadmgef.cpp b/components/esm/loadmgef.cpp index 75a94f828a..4bc09920e9 100644 --- a/components/esm/loadmgef.cpp +++ b/components/esm/loadmgef.cpp @@ -189,6 +189,7 @@ namespace ESM void MagicEffect::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // MagicEffect record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); esm.getHNT(mIndex, "INDX"); diff --git a/components/esm/loadmgef.hpp b/components/esm/loadmgef.hpp index d718aaccf5..480478d81e 100644 --- a/components/esm/loadmgef.hpp +++ b/components/esm/loadmgef.hpp @@ -16,6 +16,7 @@ struct MagicEffect /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "MagicEffect"; } + unsigned int mRecordFlags; std::string mId; enum Flags diff --git a/components/esm/loadrace.cpp b/components/esm/loadrace.cpp index ce3cc95bf8..44dbde7742 100644 --- a/components/esm/loadrace.cpp +++ b/components/esm/loadrace.cpp @@ -21,6 +21,7 @@ namespace ESM void Race::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mPowers.mList.clear(); diff --git a/components/esm/loadrace.hpp b/components/esm/loadrace.hpp index d014744472..50fa73ad74 100644 --- a/components/esm/loadrace.hpp +++ b/components/esm/loadrace.hpp @@ -65,6 +65,7 @@ struct Race RADTstruct mData; + unsigned int mRecordFlags; std::string mId, mName, mDescription; SpellList mPowers; diff --git a/components/esm/loadregn.cpp b/components/esm/loadregn.cpp index 91ea92e305..e39887a1b1 100644 --- a/components/esm/loadregn.cpp +++ b/components/esm/loadregn.cpp @@ -11,6 +11,7 @@ namespace ESM void Region::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; while (esm.hasMoreSubs()) diff --git a/components/esm/loadregn.hpp b/components/esm/loadregn.hpp index 6f39dc0bff..74f6b123ec 100644 --- a/components/esm/loadregn.hpp +++ b/components/esm/loadregn.hpp @@ -45,6 +45,7 @@ struct Region WEATstruct mData; int mMapColor; // RGBA + unsigned int mRecordFlags; // sleepList refers to a leveled list of creatures you can meet if // you sleep outside in this region. std::string mId, mName, mSleepList; diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 19602fef62..8715c83dcb 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -83,6 +83,7 @@ namespace ESM void Script::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mVarNames.clear(); diff --git a/components/esm/loadscpt.hpp b/components/esm/loadscpt.hpp index e1ffe1b864..d518a048ff 100644 --- a/components/esm/loadscpt.hpp +++ b/components/esm/loadscpt.hpp @@ -35,6 +35,7 @@ public: Script::SCHDstruct mData; }; + unsigned int mRecordFlags; std::string mId; SCHDstruct mData; diff --git a/components/esm/loadskil.cpp b/components/esm/loadskil.cpp index 61cca7d0d7..9f58176f35 100644 --- a/components/esm/loadskil.cpp +++ b/components/esm/loadskil.cpp @@ -130,6 +130,7 @@ namespace ESM void Skill::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; // Skill record can't be deleted now (may be changed in the future) + mRecordFlags = esm.getRecordFlags(); bool hasIndex = false; bool hasData = false; diff --git a/components/esm/loadskil.hpp b/components/esm/loadskil.hpp index 099264fab7..ae44a51045 100644 --- a/components/esm/loadskil.hpp +++ b/components/esm/loadskil.hpp @@ -22,6 +22,7 @@ struct Skill /// Return a string descriptor for this record type. Currently used for debugging / error logs only. static std::string getRecordType() { return "Skill"; } + unsigned int mRecordFlags; std::string mId; struct SKDTstruct diff --git a/components/esm/loadsndg.cpp b/components/esm/loadsndg.cpp index c6ea930a20..c439d0ca66 100644 --- a/components/esm/loadsndg.cpp +++ b/components/esm/loadsndg.cpp @@ -11,6 +11,7 @@ namespace ESM void SoundGenerator::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadsndg.hpp b/components/esm/loadsndg.hpp index 70b221e98c..99aae06e0e 100644 --- a/components/esm/loadsndg.hpp +++ b/components/esm/loadsndg.hpp @@ -34,6 +34,7 @@ struct SoundGenerator // Type int mType; + unsigned int mRecordFlags; std::string mId, mCreature, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadsoun.cpp b/components/esm/loadsoun.cpp index ccb5f6fdce..ed8fc519a6 100644 --- a/components/esm/loadsoun.cpp +++ b/components/esm/loadsoun.cpp @@ -11,6 +11,7 @@ namespace ESM void Sound::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasName = false; bool hasData = false; diff --git a/components/esm/loadsoun.hpp b/components/esm/loadsoun.hpp index 937e22be88..14f1178650 100644 --- a/components/esm/loadsoun.hpp +++ b/components/esm/loadsoun.hpp @@ -21,6 +21,7 @@ struct Sound static std::string getRecordType() { return "Sound"; } SOUNstruct mData; + unsigned int mRecordFlags; std::string mId, mSound; void load(ESMReader &esm, bool &isDeleted); diff --git a/components/esm/loadspel.cpp b/components/esm/loadspel.cpp index 34e146501c..5983cdcdf5 100644 --- a/components/esm/loadspel.cpp +++ b/components/esm/loadspel.cpp @@ -11,6 +11,7 @@ namespace ESM void Spell::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); mEffects.mList.clear(); diff --git a/components/esm/loadspel.hpp b/components/esm/loadspel.hpp index 1763d0991c..ef74c2c312 100644 --- a/components/esm/loadspel.hpp +++ b/components/esm/loadspel.hpp @@ -42,6 +42,7 @@ struct Spell }; SPDTstruct mData; + unsigned int mRecordFlags; std::string mId, mName; EffectList mEffects; diff --git a/components/esm/loadsscr.cpp b/components/esm/loadsscr.cpp index f436c32a1c..9e060ab1a7 100644 --- a/components/esm/loadsscr.cpp +++ b/components/esm/loadsscr.cpp @@ -11,6 +11,7 @@ namespace ESM void StartScript::load(ESMReader &esm, bool &isDeleted) { isDeleted = false; + mRecordFlags = esm.getRecordFlags(); bool hasData = false; bool hasName = false; diff --git a/components/esm/loadsscr.hpp b/components/esm/loadsscr.hpp index ce2ff49e77..3e84027076 100644 --- a/components/esm/loadsscr.hpp +++ b/components/esm/loadsscr.hpp @@ -24,6 +24,7 @@ struct StartScript static std::string getRecordType() { return "StartScript"; } std::string mData; + unsigned int mRecordFlags; std::string mId; // Load a record and add it to the list diff --git a/components/esm/magiceffects.cpp b/components/esm/magiceffects.cpp index 898e7e4b18..a1f943a93d 100644 --- a/components/esm/magiceffects.cpp +++ b/components/esm/magiceffects.cpp @@ -8,10 +8,11 @@ namespace ESM void MagicEffects::save(ESMWriter &esm) const { - for (std::map::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) + for (const auto& [key, params] : mEffects) { - esm.writeHNT("EFID", it->first); - esm.writeHNT("BASE", it->second); + esm.writeHNT("EFID", key); + esm.writeHNT("BASE", params.first); + esm.writeHNT("MODI", params.second); } } @@ -19,10 +20,15 @@ void MagicEffects::load(ESMReader &esm) { while (esm.isNextSub("EFID")) { - int id, base; + int id; + std::pair params; esm.getHT(id); - esm.getHNT(base, "BASE"); - mEffects.insert(std::make_pair(id, base)); + esm.getHNT(params.first, "BASE"); + if(esm.getFormat() < 17) + params.second = 0.f; + else + esm.getHNT(params.second, "MODI"); + mEffects.emplace(id, params); } } diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp index 5b8b0c924a..4b54692c5f 100644 --- a/components/esm/magiceffects.hpp +++ b/components/esm/magiceffects.hpp @@ -12,8 +12,8 @@ namespace ESM // format 0, saved games only struct MagicEffects { - // - std::map mEffects; + // + std::map> mEffects; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/projectilestate.cpp b/components/esm/projectilestate.cpp index 8ade9d5b2e..3421c19526 100644 --- a/components/esm/projectilestate.cpp +++ b/components/esm/projectilestate.cpp @@ -28,6 +28,7 @@ namespace ESM esm.writeHNString ("SPEL", mSpellId); esm.writeHNT ("SPED", mSpeed); + esm.writeHNT ("SLOT", mSlot); } void MagicBoltState::load(ESMReader &esm) @@ -39,6 +40,10 @@ namespace ESM esm.skipHSub(); ESM::EffectList().load(esm); // for backwards compatibility esm.getHNT (mSpeed, "SPED"); + if(esm.getFormat() < 17) + mSlot = 0; + else + esm.getHNT(mSlot, "SLOT"); if (esm.isNextSub("STCK")) // for backwards compatibility esm.skipHSub(); if (esm.isNextSub("SOUN")) // for backwards compatibility diff --git a/components/esm/projectilestate.hpp b/components/esm/projectilestate.hpp index 67ec89bb6d..84292813ce 100644 --- a/components/esm/projectilestate.hpp +++ b/components/esm/projectilestate.hpp @@ -32,6 +32,7 @@ namespace ESM { std::string mSpellId; float mSpeed; + int mSlot; void load (ESMReader &esm); void save (ESMWriter &esm) const; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3f8bf10c56..8a98a63419 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 = 16; +int ESM::SavedGame::sCurrentFormat = 17; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/esm/spellstate.cpp b/components/esm/spellstate.cpp index 2eb1e78679..b1ddb6523c 100644 --- a/components/esm/spellstate.cpp +++ b/components/esm/spellstate.cpp @@ -8,29 +8,38 @@ namespace ESM void SpellState::load(ESMReader &esm) { - while (esm.isNextSub("SPEL")) + if(esm.getFormat() < 17) { - std::string id = esm.getHString(); - - SpellParams state; - while (esm.isNextSub("INDX")) + while (esm.isNextSub("SPEL")) { - int index; - esm.getHT(index); + std::string id = esm.getHString(); - float magnitude; - esm.getHNT(magnitude, "RAND"); + SpellParams state; + while (esm.isNextSub("INDX")) + { + int index; + esm.getHT(index); - state.mEffectRands[index] = magnitude; + float magnitude; + esm.getHNT(magnitude, "RAND"); + + state.mEffectRands[index] = magnitude; + } + + while (esm.isNextSub("PURG")) { + int index; + esm.getHT(index); + state.mPurgedEffects.insert(index); + } + + mSpellParams[id] = state; + mSpells.emplace_back(id); } - - while (esm.isNextSub("PURG")) { - int index; - esm.getHT(index); - state.mPurgedEffects.insert(index); - } - - mSpells[id] = state; + } + else + { + while (esm.isNextSub("SPEL")) + mSpells.emplace_back(esm.getHString()); } // Obsolete @@ -88,30 +97,8 @@ namespace ESM void SpellState::save(ESMWriter &esm) const { - for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it) - { - esm.writeHNString("SPEL", it->first); - - const std::map& random = it->second.mEffectRands; - for (std::map::const_iterator rIt = random.begin(); rIt != random.end(); ++rIt) - { - esm.writeHNT("INDX", rIt->first); - esm.writeHNT("RAND", rIt->second); - } - - const std::set& purges = it->second.mPurgedEffects; - for (std::set::const_iterator pIt = purges.begin(); pIt != purges.end(); ++pIt) - esm.writeHNT("PURG", *pIt); - } - - for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) - { - esm.writeHNString("CORP", it->first); - - const CorprusStats & stats = it->second; - esm.writeHNT("WORS", stats.mWorsenings); - esm.writeHNT("TIME", stats.mNextWorsening); - } + for (const std::string& spell : mSpells) + esm.writeHNString("SPEL", spell); for (std::map::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it) { diff --git a/components/esm/spellstate.hpp b/components/esm/spellstate.hpp index 55c57611a2..e7067dae8c 100644 --- a/components/esm/spellstate.hpp +++ b/components/esm/spellstate.hpp @@ -31,13 +31,13 @@ namespace ESM struct SpellParams { - std::map mEffectRands; - std::set mPurgedEffects; + std::map mEffectRands; // + std::set mPurgedEffects; // indices of purged effects }; - typedef std::map TContainer; - TContainer mSpells; + std::vector mSpells; // FIXME: obsolete, used only for old saves + std::map mSpellParams; std::map > mPermanentSpellEffects; std::map mCorprusSpells; diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 92d35a6b65..35679ef293 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -100,6 +100,17 @@ boost::program_options::variables_map ConfigurationManager::separateComposingVar void ConfigurationManager::mergeComposingVariables(boost::program_options::variables_map & first, boost::program_options::variables_map & second, boost::program_options::options_description& description) { + // There are a few places this assumes all variables are present in second, but it's never crashed in the wild, so it looks like that's guaranteed. + std::set replacedVariables; + if (description.find_nothrow("replace", false)) + { + auto replace = second["replace"]; + if (!replace.defaulted() && !replace.empty()) + { + std::vector replaceVector = replace.as().toStdStringVector(); + replacedVariables.insert(replaceVector.begin(), replaceVector.end()); + } + } for (const auto& option : description.options()) { if (option->semantic()->is_composing()) @@ -113,6 +124,12 @@ void ConfigurationManager::mergeComposingVariables(boost::program_options::varia continue; } + if (replacedVariables.count(name)) + { + firstPosition->second = second[name]; + continue; + } + if (second[name].defaulted() || second[name].empty()) continue; diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index 487b7bd70b..da43cc38ec 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -17,6 +17,7 @@ #include +#include #include #include @@ -191,24 +192,10 @@ namespace Gui void FontLoader::loadBitmapFonts(bool exportToFile) { - const std::map& index = mVFS->getIndex(); - - std::string pattern = "Fonts/"; - mVFS->normalizeFilename(pattern); - - std::map::const_iterator found = index.lower_bound(pattern); - while (found != index.end()) + for (const auto& name : mVFS->getRecursiveDirectoryIterator("Fonts/")) { - const std::string& name = found->first; - if (name.size() >= pattern.size() && name.substr(0, pattern.size()) == pattern) - { - size_t pos = name.find_last_of('.'); - if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".fnt") == 0) - loadBitmapFont(name, exportToFile); - } - else - break; - ++found; + if (Misc::getFileExtension(name) == "fnt") + loadBitmapFont(name, exportToFile); } } diff --git a/components/interpreter/docs/vmformat.txt b/components/interpreter/docs/vmformat.txt index b5c9cf0ae0..7eac8b26e0 100644 --- a/components/interpreter/docs/vmformat.txt +++ b/components/interpreter/docs/vmformat.txt @@ -77,7 +77,7 @@ op 15: div (integer) stack[1] by stack[0], pop twice, push result op 16: div (float) stack[1] by stack[0], pop twice, push result op 17: convert stack[1] from integer to float op 18: convert stack[1] from float to integer -op 19: take square root of stack[0] (float) +opcode 19 unused op 20: return op 21: replace stack[0] with local short stack[0] op 22: replace stack[0] with local long stack[0] diff --git a/components/interpreter/installopcodes.cpp b/components/interpreter/installopcodes.cpp index afee36bc28..b5cb229e84 100644 --- a/components/interpreter/installopcodes.cpp +++ b/components/interpreter/installopcodes.cpp @@ -59,7 +59,6 @@ namespace Interpreter interpreter.installSegment5 (14, new OpMulInt); interpreter.installSegment5 (15, new OpDivInt); interpreter.installSegment5 (16, new OpDivInt); - interpreter.installSegment5 (19, new OpSquareRoot); interpreter.installSegment5 (26, new OpCompare >); interpreter.installSegment5 (27, diff --git a/components/interpreter/mathopcodes.hpp b/components/interpreter/mathopcodes.hpp index 42cb486b9c..bf580c6c2e 100644 --- a/components/interpreter/mathopcodes.hpp +++ b/components/interpreter/mathopcodes.hpp @@ -74,24 +74,6 @@ namespace Interpreter } }; - class OpSquareRoot : public Opcode0 - { - public: - - void execute (Runtime& runtime) override - { - Type_Float value = runtime[0].mFloat; - - if (value<0) - throw std::runtime_error ( - "square root of negative number (we aren't that imaginary)"); - - value = std::sqrt (value); - - runtime[0].mFloat = value; - } - }; - template class OpCompare : public Opcode0 { @@ -105,7 +87,7 @@ namespace Interpreter runtime[0].mInteger = result; } - }; + }; } #endif diff --git a/components/loadinglistener/reporter.cpp b/components/loadinglistener/reporter.cpp new file mode 100644 index 0000000000..0ad04fded1 --- /dev/null +++ b/components/loadinglistener/reporter.cpp @@ -0,0 +1,41 @@ +#include "reporter.hpp" +#include "loadinglistener.hpp" + +#include +#include +#include + +namespace Loading +{ + void Reporter::addTotal(std::size_t value) + { + const std::lock_guard lock(mMutex); + mTotal += value; + mUpdated.notify_all(); + } + + void Reporter::addProgress(std::size_t value) + { + const std::lock_guard lock(mMutex); + mProgress += value; + mUpdated.notify_all(); + } + + void Reporter::complete() + { + const std::lock_guard lock(mMutex); + mDone = true; + mUpdated.notify_all(); + } + + void Reporter::wait(Listener& listener) const + { + std::unique_lock lock(mMutex); + while (!mDone) + { + listener.setProgressRange(mTotal); + listener.setProgress(mProgress); + mUpdated.wait(lock); + } + } +} diff --git a/components/loadinglistener/reporter.hpp b/components/loadinglistener/reporter.hpp new file mode 100644 index 0000000000..b59c519082 --- /dev/null +++ b/components/loadinglistener/reporter.hpp @@ -0,0 +1,32 @@ +#ifndef COMPONENTS_LOADINGLISTENER_REPORTER_H +#define COMPONENTS_LOADINGLISTENER_REPORTER_H + +#include +#include +#include + +namespace Loading +{ + class Listener; + + class Reporter + { + public: + void addTotal(std::size_t value); + + void addProgress(std::size_t value); + + void complete(); + + void wait(Listener& listener) const; + + private: + std::size_t mProgress = 0; + std::size_t mTotal = 0; + bool mDone = false; + mutable std::mutex mMutex; + mutable std::condition_variable mUpdated; + }; +} + +#endif diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 25fa3aead1..8e4719dba4 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -24,7 +24,8 @@ namespace LuaUtil LuaState::LuaState(const VFS::Manager* vfs) : mVFS(vfs) { - mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, sol::lib::string, sol::lib::table); + mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, + sol::lib::string, sol::lib::table, sol::lib::debug); mLua["math"]["randomseed"](static_cast(std::time(nullptr))); mLua["math"]["randomseed"] = sol::nil; @@ -66,25 +67,31 @@ namespace LuaUtil mSandboxEnv = sol::nil; } - sol::table LuaState::makeReadOnly(sol::table table) + sol::table makeReadOnly(sol::table table) { + if (table == sol::nil) + return table; if (table.is()) return table; // it is already userdata, no sense to wrap it again + lua_State* lua = table.lua_state(); table[sol::meta_function::index] = table; - sol::stack::push(mLua, std::move(table)); - lua_newuserdata(mLua, 0); - lua_pushvalue(mLua, -2); - lua_setmetatable(mLua, -2); - return sol::stack::pop(mLua); + sol::stack::push(lua, std::move(table)); + lua_newuserdata(lua, 0); + lua_pushvalue(lua, -2); + lua_setmetatable(lua, -2); + return sol::stack::pop(lua); } - sol::table LuaState::getMutableFromReadOnly(const sol::userdata& ro) + sol::table getMutableFromReadOnly(const sol::userdata& ro) { - sol::stack::push(mLua, ro); - lua_getmetatable(mLua, -1); - sol::table res = sol::stack::pop(mLua); - lua_pop(mLua, 1); + lua_State* lua = ro.lua_state(); + sol::stack::push(lua, ro); + int ok = lua_getmetatable(lua, -1); + assert(ok); + (void)ok; + sol::table res = sol::stack::pop(lua); + lua_pop(lua, 1); return res; } @@ -164,4 +171,14 @@ namespace LuaUtil #endif } + std::string toString(const sol::object& obj) + { + if (obj == sol::nil) + return "nil"; + else if (obj.get_type() == sol::type::string) + return "\"" + obj.as() + "\""; + else + return call(sol::state_view(obj.lua_state())["tostring"], obj); + } + } diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 6bac1612e7..8982b49b36 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -3,7 +3,6 @@ #include -#include // missing from sol/sol.hpp #include #include @@ -37,11 +36,6 @@ namespace LuaUtil // A shortcut to create a new Lua table. sol::table newTable() { return sol::table(mLua, sol::create); } - // 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. - sol::table makeReadOnly(sol::table); - sol::table getMutableFromReadOnly(const sol::userdata&); - // 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 @@ -103,6 +97,14 @@ namespace LuaUtil return getFieldOrNil(table.as()[first], str...); } + // String representation of a Lua object. Should be used for debugging/logging purposes only. + std::string toString(const sol::object&); + + // 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. + sol::table makeReadOnly(sol::table); + sol::table getMutableFromReadOnly(const sol::userdata&); + } #endif // COMPONENTS_LUA_LUASTATE_H diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index a53b1d0404..703381a453 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -16,6 +16,15 @@ namespace LuaUtil static constexpr std::string_view REGISTERED_TIMER_CALLBACKS = "_timers"; static constexpr std::string_view TEMPORARY_TIMER_CALLBACKS = "_temp_timers"; + std::string ScriptsContainer::ScriptId::toString() const + { + std::string res = mContainer->mNamePrefix; + res.push_back('['); + res.append(mPath); + res.push_back(']'); + return res; + } + ScriptsContainer::ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix) : mNamePrefix(namePrefix), mLua(*lua) { registerEngineHandlers({&mUpdateHandlers}); @@ -25,7 +34,7 @@ namespace LuaUtil void ScriptsContainer::addPackage(const std::string& packageName, sol::object package) { - API[packageName] = mLua.makeReadOnly(std::move(package)); + API[packageName] = makeReadOnly(std::move(package)); } bool ScriptsContainer::addNewScript(const std::string& path) @@ -63,7 +72,7 @@ namespace LuaUtil 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] = mLua.makeReadOnly(publicInterface); + script.as()[INTERFACE] = mPublicInterfaces[interfaceName] = makeReadOnly(publicInterface); mScriptOrder.push_back(path); mScripts[path].mInterface = std::move(script); return true; @@ -81,6 +90,7 @@ namespace LuaUtil auto scriptIter = mScripts.find(path); 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) { @@ -318,8 +328,16 @@ namespace LuaUtil std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); } + ScriptsContainer::~ScriptsContainer() + { + for (auto& [_, script] : mScripts) + script.mHiddenData[ScriptId::KEY] = sol::nil; + } + void ScriptsContainer::removeAllScripts() { + for (auto& [_, script] : mScripts) + script.mHiddenData[ScriptId::KEY] = sol::nil; mScripts.clear(); mScriptOrder.clear(); for (auto& [_, handlers] : mEngineHandlers) @@ -329,7 +347,7 @@ namespace LuaUtil mHoursTimersQueue.clear(); mPublicInterfaces.clear(); - // Assigned by mLua.makeReadOnly, but `clear` removes it, so we need to assign it again. + // Assigned by LuaUtil::makeReadOnly, but `clear` removes it, so we need to assign it again. mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 7b2b2a7aa9..69aa18e940 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -66,6 +66,8 @@ namespace LuaUtil ScriptsContainer* mContainer; std::string mPath; + + std::string toString() const; }; using TimeUnit = ESM::LuaTimer::TimeUnit; @@ -73,10 +75,10 @@ namespace LuaUtil ScriptsContainer(LuaUtil::LuaState* lua, std::string_view namePrefix); ScriptsContainer(const ScriptsContainer&) = delete; ScriptsContainer(ScriptsContainer&&) = delete; - virtual ~ScriptsContainer() {} + virtual ~ScriptsContainer(); // Adds package that will be available (via `require`) for all scripts in the container. - // Automatically applies LuaState::makeReadOnly to the package. + // Automatically applies LuaUtil::makeReadOnly to the package. void addPackage(const 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. diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp index 53b6fe3b92..2e13cfe29f 100644 --- a/components/lua/serialization.cpp +++ b/components/lua/serialization.cpp @@ -43,7 +43,7 @@ namespace LuaUtil static T getValue(std::string_view& binaryData) { if (binaryData.size() < sizeof(T)) - throw std::runtime_error("Unexpected end"); + throw std::runtime_error("Unexpected end of serialized data."); T v; std::memcpy(&v, binaryData.data(), sizeof(T)); binaryData = binaryData.substr(sizeof(T)); @@ -107,15 +107,15 @@ namespace LuaUtil if (customSerializer && customSerializer->serialize(out, data)) return; else - throw std::runtime_error("Unknown userdata"); + throw std::runtime_error("Value is not serializable."); } static void serialize(BinaryData& out, const sol::object& obj, const UserdataSerializer* customSerializer, int recursionCounter) { if (obj.get_type() == sol::type::lightuserdata) - throw std::runtime_error("light userdata is not allowed to be serialized"); + throw std::runtime_error("Light userdata is not allowed to be serialized."); if (obj.is()) - throw std::runtime_error("functions are not allowed to be serialized"); + throw std::runtime_error("Functions are not allowed to be serialized."); else if (obj.is()) serializeUserdata(out, obj, customSerializer); else if (obj.is()) @@ -144,13 +144,13 @@ namespace LuaUtil appendType(out, SerializedType::BOOLEAN); out.push_back(v); } else - throw std::runtime_error("Unknown lua type"); + throw std::runtime_error("Unknown Lua type."); } static void deserializeImpl(sol::state& lua, std::string_view& binaryData, const UserdataSerializer* customSerializer) { if (binaryData.empty()) - throw std::runtime_error("Unexpected end"); + throw std::runtime_error("Unexpected end of serialized data."); unsigned char type = binaryData[0]; binaryData = binaryData.substr(1); if (type & (CUSTOM_COMPACT_FLAG | CUSTOM_FULL_FLAG)) @@ -170,7 +170,7 @@ namespace LuaUtil std::string_view data = binaryData.substr(typeNameSize, dataSize); binaryData = binaryData.substr(typeNameSize + dataSize); if (!customSerializer || !customSerializer->deserialize(typeName, data, lua)) - throw std::runtime_error("Unknown type: " + std::string(typeName)); + throw std::runtime_error("Unknown type in serialized data: " + std::string(typeName)); return; } if (type & SHORT_STRING_FLAG) @@ -205,12 +205,12 @@ namespace LuaUtil lua_settable(lua, -3); } if (binaryData.empty()) - throw std::runtime_error("Unexpected end"); + throw std::runtime_error("Unexpected end of serialized data."); binaryData = binaryData.substr(1); return; } case SerializedType::TABLE_END: - throw std::runtime_error("Unexpected table end"); + throw std::runtime_error("Unexpected end of table during deserialization."); case SerializedType::VEC2: { float x = getValue(binaryData); @@ -227,7 +227,7 @@ namespace LuaUtil return; } } - throw std::runtime_error("Unknown type: " + std::to_string(type)); + throw std::runtime_error("Unknown type in serialized data: " + std::to_string(type)); } BinaryData serialize(const sol::object& obj, const UserdataSerializer* customSerializer) diff --git a/components/misc/pathhelpers.hpp b/components/misc/pathhelpers.hpp new file mode 100644 index 0000000000..88913a1f7b --- /dev/null +++ b/components/misc/pathhelpers.hpp @@ -0,0 +1,19 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHHELPERS_H +#define OPENMW_COMPONENTS_MISC_PATHHELPERS_H + +#include + +namespace Misc +{ + inline std::string_view getFileExtension(std::string_view file) + { + if (auto extPos = file.find_last_of('.'); extPos != std::string::npos) + { + file.remove_prefix(extPos + 1); + return file; + } + return {}; + } +} + +#endif diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5cf4378b85..4d54baafc6 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -1,6 +1,7 @@ #include "resourcehelpers.hpp" #include +#include #include @@ -138,3 +139,8 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string &resP } return mdlname; } + +bool Misc::ResourceHelpers::isHiddenMarker(std::string_view id) +{ + return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; +} diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index fa50cce228..9e87954e9d 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -2,6 +2,7 @@ #define MISC_RESOURCEHELPERS_H #include +#include namespace VFS { @@ -23,6 +24,9 @@ namespace Misc std::string correctBookartPath(const std::string &resPath, int width, int height, const VFS::Manager* vfs); /// Use "xfoo.nif" instead of "foo.nif" if available std::string correctActorModelPath(const std::string &resPath, const VFS::Manager* vfs); + + /// 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 2a865606fd..0863522356 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "utf8stream.hpp" @@ -109,18 +111,34 @@ public: return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci()); } - static bool ciEqual(const std::string &x, const std::string &y) { - if (x.size() != y.size()) { + template + static bool ciEqual(const X& x, const Y& y) + { + if (std::size(x) != std::size(y)) return false; - } - std::string::const_iterator xit = x.begin(); - std::string::const_iterator yit = y.begin(); - for (; xit != x.end(); ++xit, ++yit) { - if (toLower(*xit) != toLower(*yit)) { - return false; - } - } - return true; + return std::equal(std::begin(x), std::end(x), std::begin(y), + [] (char l, char r) { return toLower(l) == toLower(r); }); + } + + template + static auto ciEqual(const char(& x)[n], const char(& y)[n]) + { + static_assert(n > 0); + return ciEqual(std::string_view(x, n - 1), std::string_view(y, n - 1)); + } + + template + static auto ciEqual(const char(& x)[n], const T& y) + { + static_assert(n > 0); + return ciEqual(std::string_view(x, n - 1), y); + } + + template + static auto ciEqual(const T& x, const char(& y)[n]) + { + static_assert(n > 0); + return ciEqual(x, std::string_view(y, n - 1)); } static int ciCompareLen(const std::string &x, const std::string &y, size_t len) @@ -157,9 +175,9 @@ public: } /// Returns lower case copy of input string - static std::string lowerCase(const std::string &in) + static std::string lowerCase(std::string_view in) { - std::string out = in; + std::string out(in); lowerCaseInPlace(out); return out; } diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 77a5ee533b..abc170c02e 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include @@ -127,9 +128,13 @@ public: state->apply(); } + // A GUI element without an associated texture would be extremely rare. + // It is worth it to use a dummy 1x1 black texture sampler instead of either adding a conditional or relinking shaders. osg::Texture2D* texture = batch.mTexture; if(texture) state->applyTextureAttribute(0, texture); + else + state->applyTextureAttribute(0, mDummyTexture); osg::GLBufferObject* bufferobject = state->isVertexBufferObjectSupported() ? vbo->getOrCreateGLBufferObject(state->getContextID()) : nullptr; if (bufferobject) @@ -189,6 +194,10 @@ public: mStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); mStateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + mDummyTexture = new osg::Texture2D; + mDummyTexture->setInternalFormat(GL_RGB); + mDummyTexture->setTextureSize(1,1); + // need to flip tex coords since MyGUI uses DirectX convention of top left image origin osg::Matrix flipMat; flipMat.preMultTranslate(osg::Vec3f(0,1,0)); @@ -201,6 +210,7 @@ public: , mStateSet(copy.mStateSet) , mWriteTo(0) , mReadFrom(0) + , mDummyTexture(copy.mDummyTexture) { } @@ -231,6 +241,11 @@ public: mBatchVector[mWriteTo].clear(); } + osg::StateSet* getDrawableStateSet() + { + return mStateSet; + } + META_Object(osgMyGUI, Drawable) private: @@ -242,6 +257,8 @@ private: int mWriteTo; mutable int mReadFrom; + + osg::ref_ptr mDummyTexture; }; class OSGVertexBuffer : public MyGUI::IVertexBuffer @@ -417,6 +434,16 @@ void RenderManager::shutdown() mSceneRoot->removeChild(mGuiRoot); } +void RenderManager::enableShaders(Shader::ShaderManager& shaderManager) +{ + auto vertexShader = shaderManager.getShader("gui_vertex.glsl", {}, osg::Shader::VERTEX); + auto fragmentShader = shaderManager.getShader("gui_fragment.glsl", {}, osg::Shader::FRAGMENT); + auto program = shaderManager.getProgram(vertexShader, fragmentShader); + + mDrawable->getDrawableStateSet()->setAttributeAndModes(program, osg::StateAttribute::ON); + mDrawable->getDrawableStateSet()->addUniform(new osg::Uniform("diffuseMap", 0)); +} + MyGUI::IVertexBuffer* RenderManager::createVertexBuffer() { return new OSGVertexBuffer(); diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp index 3c3fb672d6..8ef9691e4f 100644 --- a/components/myguiplatform/myguirendermanager.hpp +++ b/components/myguiplatform/myguirendermanager.hpp @@ -12,6 +12,11 @@ namespace Resource class ImageManager; } +namespace Shader +{ + class ShaderManager; +} + namespace osgViewer { class Viewer; @@ -62,6 +67,8 @@ public: void initialise(); void shutdown(); + void enableShaders(Shader::ShaderManager& shaderManager); + void setScalingFactor(float factor); static RenderManager& getInstance() { return *getInstancePtr(); } diff --git a/components/nif/base.hpp b/components/nif/base.hpp index 022e5224a0..0728a46512 100644 --- a/components/nif/base.hpp +++ b/components/nif/base.hpp @@ -15,6 +15,7 @@ struct Extra : public Record { std::string name; ExtraPtr next; // Next extra data record in the list + unsigned int recordSize{0u}; void read(NIFStream *nif) override { @@ -23,7 +24,7 @@ struct Extra : public Record else if (nif->getVersion() <= NIFStream::generateVersion(4,2,2,0)) { next.read(nif); - nif->getUInt(); // Size of the record + recordSize = nif->getUInt(); } } diff --git a/components/nif/extra.cpp b/components/nif/extra.cpp index eeaf9d3ac4..a45ea8c50b 100644 --- a/components/nif/extra.cpp +++ b/components/nif/extra.cpp @@ -3,6 +3,13 @@ namespace Nif { +void NiExtraData::read(NIFStream *nif) +{ + Extra::read(nif); + if (recordSize) + nif->getChars(data, recordSize); +} + void NiStringExtraData::read(NIFStream *nif) { Extra::read(nif); diff --git a/components/nif/extra.hpp b/components/nif/extra.hpp index 6d345a18ea..f4ac1caff9 100644 --- a/components/nif/extra.hpp +++ b/components/nif/extra.hpp @@ -29,6 +29,13 @@ namespace Nif { +struct NiExtraData : public Extra +{ + std::vector data; + + void read(NIFStream *nif) override; +}; + struct NiVertWeightsExtraData : public Extra { void read(NIFStream *nif) override; diff --git a/components/nif/niffile.cpp b/components/nif/niffile.cpp index 08301ce47a..6863209988 100644 --- a/components/nif/niffile.cpp +++ b/components/nif/niffile.cpp @@ -79,6 +79,7 @@ static std::map makeFactory() factory["NiPointLight"] = {&construct , RC_NiLight }; factory["NiSpotLight"] = {&construct , RC_NiLight }; factory["NiTextureEffect"] = {&construct , RC_NiTextureEffect }; + factory["NiExtraData"] = {&construct , RC_NiExtraData }; factory["NiVertWeightsExtraData"] = {&construct , RC_NiVertWeightsExtraData }; factory["NiTextKeyExtraData"] = {&construct , RC_NiTextKeyExtraData }; factory["NiStringExtraData"] = {&construct , RC_NiStringExtraData }; diff --git a/components/nif/record.hpp b/components/nif/record.hpp index ed97acabc6..dc81eb69c7 100644 --- a/components/nif/record.hpp +++ b/components/nif/record.hpp @@ -72,6 +72,7 @@ enum RecordType RC_NiBSAnimationNode, RC_NiLight, RC_NiTextureEffect, + RC_NiExtraData, RC_NiVertWeightsExtraData, RC_NiTextKeyExtraData, RC_NiStringExtraData, diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 702ab33669..a564844ce6 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -153,37 +153,14 @@ namespace { for(size_t i = 0;i < tk->list.size();i++) { - const std::string &str = tk->list[i].text; - std::string::size_type pos = 0; - while(pos < str.length()) + std::vector results; + Misc::StringUtils::split(tk->list[i].text, results, "\r\n"); + for (std::string &result : results) { - if(::isspace(str[pos])) - { - pos++; - continue; - } - - std::string::size_type nextpos = std::min(str.find('\r', pos), str.find('\n', pos)); - if(nextpos != std::string::npos) - { - do { - nextpos--; - } while(nextpos > pos && ::isspace(str[nextpos])); - nextpos++; - } - else if(::isspace(*str.rbegin())) - { - std::string::const_iterator last = str.end(); - do { - --last; - } while(last != str.begin() && ::isspace(*last)); - nextpos = std::distance(str.begin(), ++last); - } - std::string result = str.substr(pos, nextpos-pos); + Misc::StringUtils::trim(result); Misc::StringUtils::lowerCaseInPlace(result); - textkeys.emplace(tk->list[i].time, std::move(result)); - - pos = nextpos; + if (!result.empty()) + textkeys.emplace(tk->list[i].time, std::move(result)); } } } @@ -845,8 +822,10 @@ namespace NifOsg const Nif::NiMaterialColorController* matctrl = static_cast(ctrl.getPtr()); if (matctrl->data.empty() && matctrl->interpolator.empty()) continue; - osg::ref_ptr osgctrl; auto targetColor = static_cast(matctrl->targetColor); + if (mVersion <= Nif::NIFFile::NIFVersion::VER_MW && targetColor == MaterialColorController::TargetColor::Specular) + continue; + osg::ref_ptr osgctrl; if (!matctrl->interpolator.empty()) osgctrl = new MaterialColorController(matctrl->interpolator.getPtr(), targetColor, baseMaterial); else // if (!matctrl->data.empty()) @@ -1103,8 +1082,6 @@ namespace NifOsg partsys->getDefaultParticleTemplate().setColorRange(osgParticle::rangev4(osg::Vec4f(1.f,1.f,1.f,1.f), osg::Vec4f(1.f,1.f,1.f,1.f))); partsys->getDefaultParticleTemplate().setAlphaRange(osgParticle::rangef(1.f, 1.f)); - partsys->setFreezeOnCull(true); - if (!partctrl->emitter.empty()) { osg::ref_ptr emitter = handleParticleEmitter(partctrl); @@ -1157,8 +1134,6 @@ namespace NifOsg trans->addChild(toAttach); parentNode->addChild(trans); } - // create partsys stateset in order to pass in ShaderVisitor like all other Drawables - partsys->getOrCreateStateSet(); } void handleNiGeometryData(osg::Geometry *geometry, const Nif::NiGeometryData* data, const std::vector& boundTextures, const std::string& name) @@ -1503,11 +1478,14 @@ namespace NifOsg osg::ref_ptr createEmissiveTexEnv() { osg::ref_ptr texEnv(new osg::TexEnvCombine); - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + // Sum the previous colour and the emissive colour. texEnv->setCombine_RGB(osg::TexEnvCombine::ADD); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); return texEnv; } @@ -1602,27 +1580,31 @@ namespace NifOsg else if (i == Nif::NiTexturingProperty::DarkTexture) { osg::TexEnv* texEnv = new osg::TexEnv; + // Modulate both the colour and the alpha with the dark map. texEnv->setMode(osg::TexEnv::MODULATE); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::DetailTexture) { osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setScale_RGB(2.f); - texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE); - texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setOperand1_Alpha(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setSource1_Alpha(osg::TexEnvCombine::TEXTURE); + // Modulate previous colour... texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE); - texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); + // with the detail map's colour, texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); + // and a twist: + texEnv->setScale_RGB(2.f); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } else if (i == Nif::NiTexturingProperty::BumpTexture) { + // Bump maps offset the environment map. // Set this texture to Off by default since we can't render it with the fixed-function pipeline stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF); osg::Matrix2 bumpMapMatrix(texprop->bumpMapMatrix.x(), texprop->bumpMapMatrix.y(), @@ -1632,18 +1614,22 @@ namespace NifOsg } else if (i == Nif::NiTexturingProperty::DecalTexture) { - osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; - texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE); - texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS); - texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); - texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE); - texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA); - texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); - texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); - texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); - stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); + osg::TexEnvCombine* texEnv = new osg::TexEnvCombine; + // Interpolate to the decal texture's colour... + texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE); + texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR); + // ...from the previous colour... + texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR); + // using the decal texture's alpha as the factor. + texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE); + texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA); + // Keep the previous alpha. + texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); + texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS); + texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA); + stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON); } switch (i) @@ -1812,7 +1798,7 @@ namespace NifOsg // Depth test flag stateset->setMode(GL_DEPTH_TEST, zprop->flags&1 ? osg::StateAttribute::ON : osg::StateAttribute::OFF); - osg::ref_ptr depth = new osg::Depth; + auto depth = SceneUtil::createDepth(); // Depth write flag depth->setWriteMask((zprop->flags>>1)&1); // Morrowind ignores depth test function @@ -1935,8 +1921,6 @@ namespace NifOsg void applyDrawableProperties(osg::Node* node, const std::vector& properties, SceneUtil::CompositeStateSetUpdater* composite, bool hasVertexColors, int animflags) { - osg::StateSet* stateset = node->getOrCreateStateSet(); - // Specular lighting is enabled by default, but there's a quirk... bool specEnabled = true; osg::ref_ptr mat (new osg::Material); @@ -1958,6 +1942,7 @@ namespace NifOsg case Nif::RC_NiSpecularProperty: { // Specular property can turn specular lighting off. + // FIXME: NiMaterialColorController doesn't care about this. auto specprop = static_cast(property); specEnabled = specprop->flags & 1; break; @@ -2017,15 +2002,15 @@ namespace NifOsg if (blendFunc->getDestination() == GL_DST_ALPHA) blendFunc->setDestination(GL_ONE); blendFunc = shareAttribute(blendFunc); - stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); bool noSort = (alphaprop->flags>>13)&1; if (!noSort) - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + node->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); else - stateset->setRenderBinToInherit(); + node->getOrCreateStateSet()->setRenderBinToInherit(); } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::BLENDFUNC); stateset->removeMode(GL_BLEND); @@ -2036,9 +2021,9 @@ namespace NifOsg { osg::ref_ptr alphaFunc (new osg::AlphaFunc(getTestMode((alphaprop->flags>>10)&0x7), alphaprop->data.threshold/255.f)); alphaFunc = shareAttribute(alphaFunc); - stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + node->getOrCreateStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); } - else + else if (osg::StateSet* stateset = node->getStateSet()) { stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC); stateset->removeMode(GL_ALPHA_TEST); @@ -2094,8 +2079,10 @@ namespace NifOsg mat = shareAttribute(mat); + osg::StateSet* stateset = node->getOrCreateStateSet(); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); + if (emissiveMult != 1.f) + stateset->addUniform(new osg::Uniform("emissiveMult", emissiveMult)); } }; diff --git a/components/resource/bulletshape.cpp b/components/resource/bulletshape.cpp index c6f6369dba..798a6778e6 100644 --- a/components/resource/bulletshape.cpp +++ b/components/resource/bulletshape.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace Resource { @@ -57,7 +58,7 @@ btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *s for(int i = 0;i < numShapes;++i) { btCollisionShape *child = duplicateCollisionShape(comp->getChildShape(i)); - btTransform trans = comp->getChildTransform(i); + const btTransform& trans = comp->getChildTransform(i); newShape->addChildShape(trans, child); } @@ -75,6 +76,9 @@ btCollisionShape* BulletShape::duplicateCollisionShape(const btCollisionShape *s return new btBoxShape(*boxshape); } + if (shape->getShapeType() == TERRAIN_SHAPE_PROXYTYPE) + return new btHeightfieldTerrainShape(static_cast(*shape)); + throw std::logic_error(std::string("Unhandled Bullet shape duplication: ")+shape->getName()); } diff --git a/components/resource/bulletshapemanager.cpp b/components/resource/bulletshapemanager.cpp index b1b0f74176..0d0f81962b 100644 --- a/components/resource/bulletshapemanager.cpp +++ b/components/resource/bulletshapemanager.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -121,8 +122,7 @@ BulletShapeManager::~BulletShapeManager() osg::ref_ptr BulletShapeManager::getShape(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr shape; osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); @@ -130,12 +130,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & shape = osg::ref_ptr(static_cast(obj.get())); else { - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); - - if (ext == "nif") + if (Misc::getFileExtension(normalized) == "nif") { NifBullet::BulletNifLoader loader; shape = loader.load(*mNifFileManager->get(normalized)); @@ -180,8 +175,7 @@ osg::ref_ptr BulletShapeManager::getShape(const std::string & osg::ref_ptr BulletShapeManager::cacheInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr instance = createInstance(normalized); if (instance) @@ -191,8 +185,7 @@ osg::ref_ptr BulletShapeManager::cacheInstance(const std::s osg::ref_ptr BulletShapeManager::getInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); if (obj.get()) diff --git a/components/resource/imagemanager.cpp b/components/resource/imagemanager.cpp index 37e76359ff..a544b7b621 100644 --- a/components/resource/imagemanager.cpp +++ b/components/resource/imagemanager.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "objectcache.hpp" @@ -83,8 +84,7 @@ namespace Resource osg::ref_ptr ImageManager::getImage(const std::string &filename) { - std::string normalized = filename; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(filename); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -103,10 +103,7 @@ namespace Resource return mWarningImage; } - size_t extPos = normalized.find_last_of('.'); - std::string ext; - if (extPos != std::string::npos && extPos+1 < normalized.size()) - ext = normalized.substr(extPos+1); + const std::string ext(Misc::getFileExtension(normalized)); osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); if (!reader) { diff --git a/components/resource/keyframemanager.cpp b/components/resource/keyframemanager.cpp index 69986fbcd9..01a8639aa2 100644 --- a/components/resource/keyframemanager.cpp +++ b/components/resource/keyframemanager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "animation.hpp" @@ -30,7 +31,7 @@ namespace Resource std::vector emulatedAnimations; - for (auto animation : mAnimationManager->getAnimationList()) + for (const auto& animation : mAnimationManager->getAnimationList()) { if (animation) { @@ -40,7 +41,7 @@ namespace Resource } osg::ref_ptr mergedAnimationTrack = new Resource::Animation; - std::string animationName = animation->getName(); + const std::string animationName = animation->getName(); mergedAnimationTrack->setName(animationName); const osgAnimation::ChannelList& channels = animation->getChannels(); @@ -133,8 +134,7 @@ namespace Resource osg::ref_ptr KeyframeManager::get(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + const std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -142,8 +142,7 @@ namespace Resource else { osg::ref_ptr loaded (new SceneUtil::KeyframeHolder); - std::string ext = Resource::getFileExtension(normalized); - if (ext == "kf") + if (Misc::getFileExtension(normalized) == "kf") { NifOsg::Loader::loadKf(Nif::NIFFilePtr(new Nif::NIFFile(mVFS->getNormalized(normalized), normalized)), *loaded.get()); } diff --git a/components/resource/resourcemanager.hpp b/components/resource/resourcemanager.hpp index ccb065e3bf..b2b71f4635 100644 --- a/components/resource/resourcemanager.hpp +++ b/components/resource/resourcemanager.hpp @@ -59,6 +59,7 @@ namespace Resource /// How long to keep objects in cache after no longer being referenced. void setExpiryDelay (double expiryDelay) override { mExpiryDelay = expiryDelay; } + float getExpiryDelay() const { return mExpiryDelay; } const VFS::Manager* getVFS() const { return mVFS; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index f6035a47dd..be32539d40 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -17,6 +18,7 @@ #include #include +#include #include #include @@ -26,6 +28,7 @@ #include #include #include +#include #include #include @@ -33,7 +36,6 @@ #include "imagemanager.hpp" #include "niffilemanager.hpp" #include "objectcache.hpp" -#include "multiobjectcache.hpp" namespace { @@ -216,7 +218,88 @@ namespace Resource int mMaxAnisotropy; }; + // Check Collada extra descriptions + class ColladaAlphaTrickVisitor : public osg::NodeVisitor + { + public: + ColladaAlphaTrickVisitor() + : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + { + } + osg::AlphaFunc::ComparisonFunction getTestMode(std::string mode) + { + if (mode == "ALWAYS") return osg::AlphaFunc::ALWAYS; + if (mode == "LESS") return osg::AlphaFunc::LESS; + if (mode == "EQUAL") return osg::AlphaFunc::EQUAL; + if (mode == "LEQUAL") return osg::AlphaFunc::LEQUAL; + if (mode == "GREATER") return osg::AlphaFunc::GREATER; + if (mode == "NOTEQUAL") return osg::AlphaFunc::NOTEQUAL; + if (mode == "GEQUAL") return osg::AlphaFunc::GEQUAL; + if (mode == "NEVER") return osg::AlphaFunc::NEVER; + + Log(Debug::Warning) << "Unexpected alpha testing mode: " << mode; + return osg::AlphaFunc::LEQUAL; + } + + void apply(osg::Node& node) override + { + if (osg::StateSet* stateset = node.getStateSet()) + { + if (stateset->getRenderingHint() == osg::StateSet::TRANSPARENT_BIN) + { + osg::ref_ptr depth = SceneUtil::createDepth(); + depth->setWriteMask(false); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + else if (stateset->getRenderingHint() == osg::StateSet::OPAQUE_BIN) + { + osg::ref_ptr depth = SceneUtil::createDepth(); + depth->setWriteMask(true); + + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + } + + /* Check if the has + correct format for OpenMW: alphatest mode value MaterialName + e.g alphatest GEQUAL 0.8 MyAlphaTestedMaterial */ + std::vector descriptions = node.getDescriptions(); + for (auto description : descriptions) + { + mDescriptions.emplace_back(description); + } + + // Iterate each description, and see if the current node uses the specified material for alpha testing + if (node.getStateSet()) + { + for (auto description : mDescriptions) + { + std::vector descriptionParts; + std::istringstream descriptionStringStream(description); + for (std::string part; std::getline(descriptionStringStream, part, ' ');) + { + descriptionParts.emplace_back(part); + } + + if (descriptionParts.size() > (3) && descriptionParts.at(3) == node.getStateSet()->getName()) + { + if (descriptionParts.at(0) == "alphatest") + { + osg::AlphaFunc::ComparisonFunction mode = getTestMode(descriptionParts.at(1)); + osg::ref_ptr alphaFunc (new osg::AlphaFunc(mode, std::stod(descriptionParts.at(2)))); + node.getStateSet()->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON); + } + } + } + } + + traverse(node); + } + private: + std::vector mDescriptions; + }; SceneManager::SceneManager(const VFS::Manager *vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) : ResourceManager(vfs) @@ -228,7 +311,7 @@ namespace Resource , mApplyLightingToEnvMaps(false) , mLightingMethod(SceneUtil::LightingMethod::FFP) , mConvertAlphaTestToAlphaToCoverage(false) - , mInstanceCache(new MultiObjectCache) + , mDepthFormat(0) , mSharedStateManager(new SharedStateManager) , mImageManager(imageManager) , mNifFileManager(nifFileManager) @@ -250,10 +333,11 @@ namespace Resource return mForceShaders; } - void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix, bool translucentFramebuffer, bool forceShadersForNode) + void SceneManager::recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix, bool translucentFramebuffer, bool forceShadersForNode, const osg::Program* programTemplate) { osg::ref_ptr shaderVisitor(createShaderVisitor(shaderPrefix, translucentFramebuffer)); shaderVisitor->setAllowedToModifyStateSets(false); + shaderVisitor->setProgramTemplate(programTemplate); if (forceShadersForNode) shaderVisitor->setForceShaders(true); node->accept(*shaderVisitor); @@ -275,6 +359,16 @@ namespace Resource return mClampLighting; } + void SceneManager::setDepthFormat(GLenum format) + { + mDepthFormat = format; + } + + GLenum SceneManager::getDepthFormat() const + { + return mDepthFormat; + } + void SceneManager::setAutoUseNormalMaps(bool use) { mAutoUseNormalMaps = use; @@ -318,6 +412,13 @@ namespace Resource void SceneManager::setLightingMethod(SceneUtil::LightingMethod method) { mLightingMethod = method; + + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) + { + osg::ref_ptr program = new osg::Program; + program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); + mShaderManager->setProgramTemplate(program); + } } SceneUtil::LightingMethod SceneManager::getLightingMethod() const @@ -347,10 +448,7 @@ namespace Resource bool SceneManager::checkLoaded(const std::string &name, double timeStamp) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - return mCache->checkInObjectCache(normalized, timeStamp); + return mCache->checkInObjectCache(mVFS->normalizeFilename(name), timeStamp); } /// @brief Callback to read image files from the VFS. @@ -380,12 +478,12 @@ namespace Resource osg::ref_ptr load (const std::string& normalizedFilename, const VFS::Manager* vfs, Resource::ImageManager* imageManager, Resource::NifFileManager* nifFileManager) { - std::string ext = Resource::getFileExtension(normalizedFilename); + auto ext = Misc::getFileExtension(normalizedFilename); if (ext == "nif") return NifOsg::Loader::load(nifFileManager->get(normalizedFilename), imageManager); else { - osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(ext); + osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension(std::string(ext)); if (!reader) { std::stringstream errormsg; @@ -415,6 +513,18 @@ namespace Resource if (nameFinder.mFoundNode) nameFinder.mFoundNode->setNodeMask(hiddenNodeMask); + if (ext == "dae") + { + // Collada alpha testing + Resource::ColladaAlphaTrickVisitor colladaAlphaTrickVisitor; + result.getNode()->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)); + } + + return result.getNode(); } } @@ -522,8 +632,7 @@ namespace Resource osg::ref_ptr SceneManager::getTemplate(const std::string &name, bool compile) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); + std::string normalized = mVFS->normalizeFilename(name); osg::ref_ptr obj = mCache->getRefFromObjectCache(normalized); if (obj) @@ -563,22 +672,18 @@ namespace Resource osg::ref_ptr shaderVisitor (createShaderVisitor()); loaded->accept(*shaderVisitor); - // share state - // do this before optimizing so the optimizer will be able to combine nodes more aggressively - // note, because StateSets will be shared at this point, StateSets can not be modified inside the optimizer - mSharedStateMutex.lock(); - mSharedStateManager->share(loaded.get()); - mSharedStateMutex.unlock(); - if (canOptimize(normalized)) { SceneUtil::Optimizer optimizer; + optimizer.setSharedStateManager(mSharedStateManager, &mSharedStateMutex); optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); - static const unsigned int options = getOptimizationOptions(); + static const unsigned int options = getOptimizationOptions()|SceneUtil::Optimizer::SHARE_DUPLICATE_STATE; optimizer.optimize(loaded, options); } + else + shareState(loaded); if (compile && mIncrementalCompileOperation) mIncrementalCompileOperation->add(loaded); @@ -590,22 +695,6 @@ namespace Resource } } - osg::ref_ptr SceneManager::cacheInstance(const std::string &name) - { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr node = createInstance(normalized); - - // Note: osg::clone() does not calculate bound volumes. - // Do it immediately, otherwise we will need to update them for all objects - // during first update traversal, what may lead to stuttering during cell transitions - node->getBound(); - - mInstanceCache->addEntryToObjectCache(normalized, node.get()); - return node; - } - osg::ref_ptr SceneManager::createInstance(const std::string& name) { osg::ref_ptr scene = getTemplate(name); @@ -631,15 +720,7 @@ namespace Resource osg::ref_ptr SceneManager::getInstance(const std::string &name) { - std::string normalized = name; - mVFS->normalizeFilename(normalized); - - osg::ref_ptr obj = mInstanceCache->takeFromObjectCache(normalized); - if (obj.get()) - return static_cast(obj.get()); - - return createInstance(normalized); - + return createInstance(name); } osg::ref_ptr SceneManager::getInstance(const std::string &name, osg::Group* parentNode) @@ -657,7 +738,6 @@ namespace Resource void SceneManager::releaseGLObjects(osg::State *state) { mCache->releaseGLObjects(state); - mInstanceCache->releaseGLObjects(state); mShaderManager->releaseGLObjects(state); @@ -745,8 +825,6 @@ namespace Resource { ResourceManager::updateCache(referenceTime); - mInstanceCache->removeUnreferencedObjectsInCache(); - mSharedStateMutex.lock(); mSharedStateManager->prune(); mSharedStateMutex.unlock(); @@ -776,7 +854,6 @@ namespace Resource std::lock_guard lock(mSharedStateMutex); mSharedStateManager->clearCache(); - mInstanceCache->clear(); } void SceneManager::reportStats(unsigned int frameNumber, osg::Stats *stats) const @@ -794,7 +871,6 @@ namespace Resource } stats->setAttribute(frameNumber, "Node", mCache->getCacheSize()); - stats->setAttribute(frameNumber, "Node Instance", mInstanceCache->getCacheSize()); } Shader::ShaderVisitor *SceneManager::createShaderVisitor(const std::string& shaderPrefix, bool translucentFramebuffer) @@ -811,12 +887,4 @@ namespace Resource shaderVisitor->setTranslucentFramebuffer(translucentFramebuffer); return shaderVisitor; } - - std::string getFileExtension(const std::string& file) - { - size_t extPos = file.find_last_of('.'); - if (extPos != std::string::npos && extPos+1 < file.size()) - return file.substr(extPos+1); - return std::string(); - } } diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index de014165bf..1443476fd5 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -65,8 +65,6 @@ namespace Resource std::vector> mObjects; }; - class MultiObjectCache; - /// @brief Handles loading and caching of scenes, e.g. .nif files or .osg files /// @note Some methods of the scene manager can be used from any thread, see the methods documentation for more details. class SceneManager : public ResourceManager @@ -78,7 +76,7 @@ namespace Resource Shader::ShaderManager& getShaderManager(); /// Re-create shaders for this node, need to call this if alpha testing, texture stages or vertex color mode have changed. - void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false, bool forceShadersForNode = false); + void recreateShaders(osg::ref_ptr node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false, bool forceShadersForNode = false, const osg::Program* programTemplate = nullptr); /// Applying shaders to a node may replace some fixed-function state. /// This restores it. @@ -92,6 +90,9 @@ namespace Resource void setClampLighting(bool clamp); bool getClampLighting() const; + void setDepthFormat(GLenum format); + GLenum getDepthFormat() const; + /// @see ShaderVisitor::setAutoUseNormalMaps void setAutoUseNormalMaps(bool use); @@ -110,6 +111,11 @@ namespace Resource void setSupportedLightingMethods(const SceneUtil::LightManager::SupportedMethods& supported); bool isSupportedLightingMethod(SceneUtil::LightingMethod method) const; + enum class UBOBinding + { + // If we add more UBO's, we should probably assign their bindings dynamically according to the current count of UBO's in the programTemplate + LightBuffer + }; void setLightingMethod(SceneUtil::LightingMethod method); SceneUtil::LightingMethod getLightingMethod() const; @@ -126,12 +132,6 @@ namespace Resource /// @note Thread safe. osg::ref_ptr getTemplate(const std::string& name, bool compile=true); - /// Create an instance of the given scene template and cache it for later use, so that future calls to getInstance() can simply - /// return this cached object instead of creating a new one. - /// @note The returned ref_ptr may be kept around by the caller to ensure that the object stays in cache for as long as needed. - /// @note Thread safe. - osg::ref_ptr cacheInstance(const std::string& name); - osg::ref_ptr createInstance(const std::string& name); osg::ref_ptr createInstance(const osg::Node* base); @@ -202,8 +202,7 @@ namespace Resource SceneUtil::LightingMethod mLightingMethod; SceneUtil::LightManager::SupportedMethods mSupportedLightingMethods; bool mConvertAlphaTestToAlphaToCoverage; - - osg::ref_ptr mInstanceCache; + GLenum mDepthFormat; osg::ref_ptr mSharedStateManager; mutable std::mutex mSharedStateMutex; diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index 4d07889d0c..b3705f69cc 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -376,7 +376,6 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "Texture", "StateSet", "Node", - "Node Instance", "Shape", "Shape Instance", "Image", @@ -390,7 +389,10 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "Land", "Composite", "", - "NavMesh UpdateJobs", + "NavMesh Jobs", + "NavMesh Waiting", + "NavMesh Pushed", + "NavMesh Processing", "NavMesh CacheSize", "NavMesh UsedTiles", "NavMesh CachedTiles", @@ -401,6 +403,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer) "", "Physics Actors", "Physics Objects", + "Physics Projectiles", "Physics HeightFields", }); diff --git a/components/sceneutil/agentpath.cpp b/components/sceneutil/agentpath.cpp index abe332f758..5f9b574e7d 100644 --- a/components/sceneutil/agentpath.cpp +++ b/components/sceneutil/agentpath.cpp @@ -1,6 +1,8 @@ #include "agentpath.hpp" #include "detourdebugdraw.hpp" +#include + #include #include @@ -65,6 +67,10 @@ namespace SceneUtil debugDraw.depthMask(true); + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + group->getOrCreateStateSet()->setAttribute(material); + return group; } } diff --git a/components/sceneutil/attach.cpp b/components/sceneutil/attach.cpp index 597c7adf48..6690148c74 100644 --- a/components/sceneutil/attach.cpp +++ b/components/sceneutil/attach.cpp @@ -15,6 +15,7 @@ #include #include "visitor.hpp" +#include "clone.hpp" namespace SceneUtil { @@ -49,10 +50,10 @@ namespace SceneUtil return; osg::Node* node = &drawable; - while (node->getNumParents()) + for (auto it = getNodePath().rbegin()+1; it != getNodePath().rend(); ++it) { - osg::Group* parent = node->getParent(0); - if (!parent || !filterMatches(parent->getName())) + osg::Node* parent = *it; + if (!filterMatches(parent->getName())) break; node = parent; } @@ -63,12 +64,7 @@ namespace SceneUtil { for (const osg::ref_ptr& node : mToCopy) { - if (node->getNumParents() > 1) - Log(Debug::Error) << "Error CopyRigVisitor: node has " << node->getNumParents() << " parents"; - while (node->getNumParents()) - node->getParent(0)->removeChild(node); - - mParent->addChild(node); + mParent->addChild(static_cast(node->clone(SceneUtil::CopyOp()))); } mToCopy.clear(); } @@ -90,25 +86,28 @@ namespace SceneUtil std::string mFilter2; }; - void mergeUserData(osg::UserDataContainer* source, osg::Object* target) + void mergeUserData(const osg::UserDataContainer* source, osg::Object* target) { + if (!source) + return; + if (!target->getUserDataContainer()) - target->setUserDataContainer(source); + target->setUserDataContainer(osg::clone(source, osg::CopyOp::SHALLOW_COPY)); else { for (unsigned int i=0; igetNumUserObjects(); ++i) - target->getUserDataContainer()->addUserObject(source->getUserObject(i)); + target->getUserDataContainer()->addUserObject(osg::clone(source->getUserObject(i), osg::CopyOp::SHALLOW_COPY)); } } - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node *master, const std::string &filter, osg::Group* attachNode) { - if (dynamic_cast(toAttach.get())) + if (dynamic_cast(toAttach.get())) { osg::ref_ptr handle = new osg::Group; CopyRigVisitor copyVisitor(handle, filter); - toAttach->accept(copyVisitor); + const_cast(toAttach.get())->accept(copyVisitor); copyVisitor.doCopy(); if (handle->getNumChildren() == 1) @@ -122,14 +121,16 @@ namespace SceneUtil else { master->asGroup()->addChild(handle); - handle->setUserDataContainer(toAttach->getUserDataContainer()); + mergeUserData(toAttach->getUserDataContainer(), handle); return handle; } } else { + osg::ref_ptr clonedToAttach = static_cast(toAttach->clone(SceneUtil::CopyOp())); + FindByNameVisitor findBoneOffset("BoneOffset"); - toAttach->accept(findBoneOffset); + clonedToAttach->accept(findBoneOffset); osg::ref_ptr trans; @@ -172,13 +173,13 @@ namespace SceneUtil if (trans) { attachNode->addChild(trans); - trans->addChild(toAttach); + trans->addChild(clonedToAttach); return trans; } else { - attachNode->addChild(toAttach); - return toAttach; + attachNode->addChild(clonedToAttach); + return clonedToAttach; } } } diff --git a/components/sceneutil/attach.hpp b/components/sceneutil/attach.hpp index a8a2239a84..806fc53488 100644 --- a/components/sceneutil/attach.hpp +++ b/components/sceneutil/attach.hpp @@ -14,12 +14,12 @@ namespace osg namespace SceneUtil { - /// Attach parts of the \a toAttach scenegraph to the \a master scenegraph, using the specified filter and attachment node. + /// Clone and attach parts of the \a toAttach scenegraph to the \a master scenegraph, using the specified filter and attachment node. /// If the \a toAttach scene graph contains skinned objects, we will attach only those (filtered by the \a filter). /// Otherwise, just attach all of the toAttach scenegraph to the attachment node on the master scenegraph, with no filtering. /// @note The master scene graph is expected to include a skeleton. /// @return A newly created node that is directly attached to the master scene graph - osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); + osg::ref_ptr attach(osg::ref_ptr toAttach, osg::Node* master, const std::string& filter, osg::Group* attachNode); } diff --git a/components/sceneutil/clone.cpp b/components/sceneutil/clone.cpp index 07a6e691ad..eaf5668c4a 100644 --- a/components/sceneutil/clone.cpp +++ b/components/sceneutil/clone.cpp @@ -2,8 +2,6 @@ #include -#include -#include #include #include @@ -35,11 +33,6 @@ namespace SceneUtil mUpdaterToOldPs[cloned] = updater->getParticleSystem(0); return cloned; } - - if (dynamic_cast(node) || dynamic_cast(node)) - { - return osg::clone(node, *this); - } return osg::CopyOp::operator()(node); } diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 2c83c43fef..ba60cb3185 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -294,9 +295,9 @@ namespace SceneUtil osg::ref_ptr ubo = new osg::UniformBufferObject; buffer->getData()->setBufferObject(ubo); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); + 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(Shader::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), ubo, 0, buffer->getData()->getTotalDataSize()); #endif stateset->setAttributeAndModes(ubb, mode); @@ -676,9 +677,9 @@ namespace SceneUtil auto bo = mLightManager->getLightBuffer(mLastFrameNumber); #if OSG_VERSION_GREATER_OR_EQUAL(3,5,7) - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData(), 0, bo->getData()->getTotalDataSize()); #else - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), bo->getData()->getBufferObject(), 0, bo->getData()->getTotalDataSize()); #endif stateset->setAttributeAndModes(ubb, osg::StateAttribute::ON); } @@ -736,7 +737,7 @@ namespace SceneUtil // 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(Shader::UBOBinding::LightBuffer)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); } LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) @@ -751,7 +752,7 @@ namespace SceneUtil void initSharedLayout(osg::GLExtensions* ext, int handle) const { - constexpr std::array index = { static_cast(Shader::UBOBinding::LightBuffer) }; + constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; int totalBlockSize = -1; int stride = -1; @@ -884,8 +885,6 @@ namespace SceneUtil std::string lightingMethodString = Settings::Manager::getString("lighting method", "Shaders"); auto lightingMethod = LightManager::getLightingMethodFromString(lightingMethodString); - updateSettings(); - static bool hasLoggedWarnings = false; if (lightingMethod == LightingMethod::SingleUBO && !hasLoggedWarnings) @@ -904,6 +903,8 @@ namespace SceneUtil else initSingleUBO(targetLights); + updateSettings(); + getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); addCullCallback(new LightManagerCullCallback(this)); @@ -1204,7 +1205,6 @@ namespace SceneUtil const std::vector& LightManager::getLightsInViewSpace(osg::Camera *camera, const osg::RefMatrix* viewMatrix, size_t frameNum) { - bool isReflection = isReflectionCamera(camera); osg::observer_ptr camPtr (camera); auto it = mLightsInViewSpace.find(camPtr); @@ -1212,6 +1212,8 @@ 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); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index ad3fc5fd65..95ff8f2b01 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -23,8 +23,12 @@ #include #include #include +#include #include + +#include + #include "shadowsbin.hpp" namespace { @@ -1636,6 +1640,11 @@ 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); + } _shadowCastingStateSet->setAttribute(depth, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE); _shadowCastingStateSet->setMode(GL_DEPTH_CLAMP, osg::StateAttribute::ON); @@ -2132,7 +2141,7 @@ struct ConvexHull finalEdges.push_back(edge); } - _edges = finalEdges; + _edges = std::move(finalEdges); } void transform(const osg::Matrixd& m) diff --git a/components/sceneutil/navmesh.cpp b/components/sceneutil/navmesh.cpp index aeb1779bd6..b0f356f089 100644 --- a/components/sceneutil/navmesh.cpp +++ b/components/sceneutil/navmesh.cpp @@ -6,6 +6,7 @@ #include #include +#include namespace SceneUtil { @@ -17,6 +18,11 @@ namespace SceneUtil navMeshQuery.init(&navMesh, settings.mMaxNavMeshQueryNodes); duDebugDrawNavMeshWithClosedList(&debugDraw, navMesh, navMeshQuery, DU_DRAWNAVMESH_OFFMESHCONS | DU_DRAWNAVMESH_CLOSEDLIST); + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + group->getOrCreateStateSet()->setAttribute(material); + return group; } } diff --git a/components/sceneutil/optimizer.cpp b/components/sceneutil/optimizer.cpp index b48ceda409..dffbe62ec7 100644 --- a/components/sceneutil/optimizer.cpp +++ b/components/sceneutil/optimizer.cpp @@ -30,6 +30,8 @@ #include #include +#include + #include #include #include @@ -40,6 +42,8 @@ #include +#include + using namespace osgUtil; namespace SceneUtil @@ -82,6 +86,13 @@ void Optimizer::optimize(osg::Node* node, unsigned int options) cstv.removeTransforms(node); } + if (options & SHARE_DUPLICATE_STATE && _sharedStateManager) + { + if (_sharedStateMutex) _sharedStateMutex->lock(); + _sharedStateManager->share(node); + if (_sharedStateMutex) _sharedStateMutex->unlock(); + } + if (options & REMOVE_REDUNDANT_NODES) { OSG_INFO<<"Optimizer::optimize() doing REMOVE_REDUNDANT_NODES"<getNumChildren()==1 && transform->getChild(0)->asTransform()!=0 && transform->getChild(0)->asTransform()->asMatrixTransform()!=0 && - transform->getChild(0)->asTransform()->getDataVariance()==osg::Object::STATIC) + (!transform->getChild(0)->getStateSet() || transform->getChild(0)->getStateSet()->referenceCount()==1) && + transform->getChild(0)->getDataVariance()==osg::Object::STATIC) { // now combine with its child. osg::MatrixTransform* child = transform->getChild(0)->asTransform()->asMatrixTransform(); @@ -1560,7 +1572,7 @@ bool Optimizer::MergeGeometryVisitor::mergeGroup(osg::Group& group) } if (_alphaBlendingActive && _mergeAlphaBlending && !geom->getStateSet()) { - osg::Depth* d = new osg::Depth; + auto d = createDepth(); d->setWriteMask(0); geom->getOrCreateStateSet()->setAttribute(d); } diff --git a/components/sceneutil/optimizer.hpp b/components/sceneutil/optimizer.hpp index 2d6293e231..7b32bafee2 100644 --- a/components/sceneutil/optimizer.hpp +++ b/components/sceneutil/optimizer.hpp @@ -25,6 +25,12 @@ //#include #include +#include + +namespace osgDB +{ + class SharedStateManager; +} //namespace osgUtil { namespace SceneUtil { @@ -65,7 +71,7 @@ class Optimizer public: - Optimizer() : _mergeAlphaBlending(false) {} + Optimizer() : _mergeAlphaBlending(false), _sharedStateManager(nullptr), _sharedStateMutex(nullptr) {} virtual ~Optimizer() {} enum OptimizationOptions @@ -121,6 +127,8 @@ class Optimizer void setMergeAlphaBlending(bool merge) { _mergeAlphaBlending = merge; } void setViewPoint(const osg::Vec3f& viewPoint) { _viewPoint = viewPoint; } + void setSharedStateManager(osgDB::SharedStateManager* sharedStateManager, std::mutex* sharedStateMutex) { _sharedStateMutex = sharedStateMutex; _sharedStateManager = sharedStateManager; } + /** Reset internal data to initial state - the getPermissibleOptionsMap is cleared.*/ void reset(); @@ -258,6 +266,9 @@ class Optimizer osg::Vec3f _viewPoint; bool _mergeAlphaBlending; + osgDB::SharedStateManager* _sharedStateManager; + mutable std::mutex* _sharedStateMutex; + public: /** Flatten Static Transform nodes by applying their transform to the diff --git a/components/sceneutil/pathgridutil.cpp b/components/sceneutil/pathgridutil.cpp index f37a8ba597..9ebdef61b0 100644 --- a/components/sceneutil/pathgridutil.cpp +++ b/components/sceneutil/pathgridutil.cpp @@ -1,6 +1,7 @@ #include "pathgridutil.hpp" #include +#include #include @@ -174,6 +175,11 @@ namespace SceneUtil gridGeometry->addPrimitiveSet(lineIndices); gridGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); } + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + gridGeometry->getOrCreateStateSet()->setAttribute(material); + return gridGeometry; } diff --git a/components/sceneutil/recastmesh.cpp b/components/sceneutil/recastmesh.cpp index 2716f46832..97efe010df 100644 --- a/components/sceneutil/recastmesh.cpp +++ b/components/sceneutil/recastmesh.cpp @@ -3,10 +3,15 @@ #include #include +#include #include #include +#include + +#include +#include namespace { @@ -37,12 +42,35 @@ namespace SceneUtil osg::ref_ptr createRecastMeshGroup(const DetourNavigator::RecastMesh& recastMesh, const DetourNavigator::Settings& settings) { + using namespace DetourNavigator; + const osg::ref_ptr group(new osg::Group); - DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1.0f / settings.mRecastScaleFactor); - const auto normals = calculateNormals(recastMesh.getVertices(), recastMesh.getIndices()); + DebugDraw debugDraw(*group, osg::Vec3f(0, 0, 0), 1.0f); + const DetourNavigator::Mesh& mesh = recastMesh.getMesh(); + std::vector indices = mesh.getIndices(); + std::vector vertices = mesh.getVertices(); + + for (const Heightfield& heightfield : recastMesh.getHeightfields()) + { + const Mesh heightfieldMesh = makeMesh(heightfield); + const int indexShift = static_cast(vertices.size() / 3); + std::copy(heightfieldMesh.getVertices().begin(), heightfieldMesh.getVertices().end(), std::back_inserter(vertices)); + std::transform(heightfieldMesh.getIndices().begin(), heightfieldMesh.getIndices().end(), std::back_inserter(indices), + [&] (int index) { return index + indexShift; }); + } + + for (std::size_t i = 0; i < vertices.size(); i += 3) + std::swap(vertices[i + 1], vertices[i + 2]); + + const auto normals = calculateNormals(vertices, indices); const auto texScale = 1.0f / (settings.mCellSize * 10.0f); - duDebugDrawTriMesh(&debugDraw, recastMesh.getVertices().data(), recastMesh.getVerticesCount(), - recastMesh.getIndices().data(), normals.data(), recastMesh.getTrianglesCount(), nullptr, texScale); + duDebugDrawTriMesh(&debugDraw, vertices.data(), static_cast(vertices.size() / 3), + indices.data(), normals.data(), static_cast(indices.size() / 3), nullptr, texScale); + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + group->getOrCreateStateSet()->setAttribute(material); + return group; } } diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp new file mode 100644 index 0000000000..a661a90279 --- /dev/null +++ b/components/sceneutil/rtt.cpp @@ -0,0 +1,110 @@ +#include "rtt.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +#include + +namespace SceneUtil +{ + // RTTNode's cull callback + class CullCallback : public osg::NodeCallback + { + public: + CullCallback(RTTNode* group) + : mGroup(group) {} + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + mGroup->cull(cv); + } + RTTNode* mGroup; + }; + + RTTNode::RTTNode(uint32_t textureWidth, uint32_t textureHeight, int renderOrderNum, bool doPerViewMapping) + : mTextureWidth(textureWidth) + , mTextureHeight(textureHeight) + , mRenderOrderNum(renderOrderNum) + , mDoPerViewMapping(doPerViewMapping) + { + addCullCallback(new CullCallback(this)); + setCullingActive(false); + } + + RTTNode::~RTTNode() + { + } + + void RTTNode::cull(osgUtil::CullVisitor* cv) + { + auto* vdd = getViewDependentData(cv); + apply(vdd->mCamera); + vdd->mCamera->accept(*cv); + } + + osg::Texture* RTTNode::getColorTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mCamera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._texture; + } + + osg::Texture* RTTNode::getDepthTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mCamera->getBufferAttachmentMap()[osg::Camera::DEPTH_BUFFER]._texture; + } + + RTTNode::ViewDependentData* RTTNode::getViewDependentData(osgUtil::CullVisitor* cv) + { + if (!mDoPerViewMapping) + // Always setting it to null is an easy way to disable per-view mapping when mDoPerViewMapping is false. + // This is safe since the visitor is never dereferenced. + cv = nullptr; + + if (mViewDependentDataMap.count(cv) == 0) + { + auto camera = new osg::Camera(); + mViewDependentDataMap[cv].reset(new ViewDependentData); + mViewDependentDataMap[cv]->mCamera = camera; + + camera->setRenderOrder(osg::Camera::PRE_RENDER, mRenderOrderNum); + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + camera->setViewport(0, 0, mTextureWidth, mTextureHeight); + + setDefaults(mViewDependentDataMap[cv]->mCamera.get()); + + // Create any buffer attachments not added in setDefaults + if (camera->getBufferAttachmentMap().count(osg::Camera::COLOR_BUFFER) == 0) + { + auto colorBuffer = new osg::Texture2D; + colorBuffer->setTextureSize(mTextureWidth, mTextureHeight); + colorBuffer->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + colorBuffer->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + colorBuffer->setInternalFormat(GL_RGB); + colorBuffer->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + colorBuffer->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::COLOR_BUFFER, colorBuffer); + SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, colorBuffer); + } + + if (camera->getBufferAttachmentMap().count(osg::Camera::DEPTH_BUFFER) == 0) + { + auto depthBuffer = new osg::Texture2D; + depthBuffer->setTextureSize(mTextureWidth, mTextureHeight); + depthBuffer->setSourceFormat(GL_DEPTH_COMPONENT); + depthBuffer->setInternalFormat(GL_DEPTH_COMPONENT24); + depthBuffer->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + depthBuffer->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + depthBuffer->setSourceType(GL_UNSIGNED_INT); + depthBuffer->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + depthBuffer->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::DEPTH_BUFFER, depthBuffer); + } + } + + return mViewDependentDataMap[cv].get(); + } +} diff --git a/components/sceneutil/rtt.hpp b/components/sceneutil/rtt.hpp new file mode 100644 index 0000000000..7adfa098eb --- /dev/null +++ b/components/sceneutil/rtt.hpp @@ -0,0 +1,68 @@ +#ifndef OPENMW_RTT_H +#define OPENMW_RTT_H + +#include + +#include +#include + +namespace osg +{ + class Texture2D; + class Camera; +} + +namespace osgUtil +{ + class CullVisitor; +} + +namespace SceneUtil +{ + /// @brief Implements per-view RTT operations. + /// @par With a naive RTT implementation, subsequent views of multiple views will overwrite the results of the previous views, leading to + /// the results of the last view being broadcast to all views. An error in all cases where the RTT result depends on the view. + /// @par If using an RTTNode this is solved by mapping RTT operations to CullVisitors, which will be unique per view. This requires + /// instancing one camera per view, and traversing only the camera mapped to that CV during cull traversals. + /// @par Camera settings should be effectuated by overriding the setDefaults() and apply() methods, following a pattern similar to SceneUtil::StateSetUpdater + /// @par When using the RTT texture in your statesets, it is recommended to use SceneUtil::StateSetUpdater as a cull callback to handle this as the appropriate + /// textures can be retrieved during SceneUtil::StateSetUpdater::Apply() + /// @par For any of COLOR_BUFFER or DEPTH_BUFFER not added during setDefaults(), RTTNode will attach a default buffer. The default color buffer has an internal format of GL_RGB. + /// The default depth buffer has internal format GL_DEPTH_COMPONENT24, source format GL_DEPTH_COMPONENT, and source type GL_UNSIGNED_INT. Default wrap is CLAMP_TO_EDGE and filter LINEAR. + class RTTNode : public osg::Node + { + public: + RTTNode(uint32_t textureWidth, uint32_t textureHeight, int renderOrderNum, bool doPerViewMapping); + ~RTTNode(); + + osg::Texture* getColorTexture(osgUtil::CullVisitor* cv); + + osg::Texture* getDepthTexture(osgUtil::CullVisitor* cv); + + + /// Apply state - to override in derived classes + /// @note Due to the view mapping approach you *have* to apply all camera settings, even if they have not changed since the last frame. + virtual void setDefaults(osg::Camera* camera) {}; + + /// Set default settings - optionally override in derived classes + virtual void apply(osg::Camera* camera) {}; + + void cull(osgUtil::CullVisitor* cv); + + private: + struct ViewDependentData + { + osg::ref_ptr mCamera; + }; + + ViewDependentData* getViewDependentData(osgUtil::CullVisitor* cv); + + typedef std::map< osgUtil::CullVisitor*, std::unique_ptr > ViewDependentDataMap; + ViewDependentDataMap mViewDependentDataMap; + uint32_t mTextureWidth; + uint32_t mTextureHeight; + int mRenderOrderNum; + bool mDoPerViewMapping; + }; +} +#endif diff --git a/components/sceneutil/screencapture.cpp b/components/sceneutil/screencapture.cpp index 47119f3644..99cb535326 100644 --- a/components/sceneutil/screencapture.cpp +++ b/components/sceneutil/screencapture.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace { @@ -32,6 +33,9 @@ namespace void doWork() override { + if (mAborted) + return; + try { (*mImpl)(*mImage, mContextId); @@ -42,10 +46,16 @@ namespace } } + void abort() override + { + mAborted = true; + } + private: const osg::ref_ptr mImpl; const osg::ref_ptr mImage; const unsigned int mContextId; + std::atomic_bool mAborted {false}; }; } @@ -130,8 +140,27 @@ namespace SceneUtil assert(mImpl != nullptr); } + AsyncScreenCaptureOperation::~AsyncScreenCaptureOperation() + { + stop(); + } + + void AsyncScreenCaptureOperation::stop() + { + for (const osg::ref_ptr& item : *mWorkItems.lockConst()) + item->abort(); + + for (const osg::ref_ptr& item : *mWorkItems.lockConst()) + item->waitTillDone(); + } + void AsyncScreenCaptureOperation::operator()(const osg::Image& image, const unsigned int context_id) { - mQueue->addWorkItem(new ScreenCaptureWorkItem(mImpl, image, context_id)); + osg::ref_ptr item(new ScreenCaptureWorkItem(mImpl, image, context_id)); + mQueue->addWorkItem(item); + const auto isDone = [] (const osg::ref_ptr& v) { return v->isDone(); }; + const auto workItems = mWorkItems.lock(); + workItems->erase(std::remove_if(workItems->begin(), workItems->end(), isDone), workItems->end()); + workItems->emplace_back(std::move(item)); } } diff --git a/components/sceneutil/screencapture.hpp b/components/sceneutil/screencapture.hpp index 6395d989d8..87e396b020 100644 --- a/components/sceneutil/screencapture.hpp +++ b/components/sceneutil/screencapture.hpp @@ -1,10 +1,13 @@ #ifndef OPENMW_COMPONENTS_SCENEUTIL_SCREENCAPTURE_H #define OPENMW_COMPONENTS_SCENEUTIL_SCREENCAPTURE_H +#include + #include #include #include +#include namespace osg { @@ -14,6 +17,7 @@ namespace osg namespace SceneUtil { class WorkQueue; + class WorkItem; std::string writeScreenshotToFile(const std::string& screenshotPath, const std::string& screenshotFormat, const osg::Image& image); @@ -38,11 +42,16 @@ namespace SceneUtil AsyncScreenCaptureOperation(osg::ref_ptr queue, osg::ref_ptr impl); + ~AsyncScreenCaptureOperation(); + + void stop(); + void operator()(const osg::Image& image, const unsigned int context_id) override; private: const osg::ref_ptr mQueue; const osg::ref_ptr mImpl; + Misc::ScopeGuarded>> mWorkItems; }; } diff --git a/components/sceneutil/serialize.cpp b/components/sceneutil/serialize.cpp index 1b5c1feed8..703b63af7d 100644 --- a/components/sceneutil/serialize.cpp +++ b/components/sceneutil/serialize.cpp @@ -80,7 +80,7 @@ class MatrixTransformSerializer : public osgDB::ObjectWrapper { public: MatrixTransformSerializer() - : osgDB::ObjectWrapper(createInstanceFunc, "NifOsg::MatrixTransform", "osg::Object osg::Node osg::Transform osg::MatrixTransform NifOsg::MatrixTransform") + : osgDB::ObjectWrapper(createInstanceFunc, "NifOsg::MatrixTransform", "osg::Object osg::Node osg::Group osg::Transform osg::MatrixTransform NifOsg::MatrixTransform") { } }; diff --git a/components/sceneutil/shadowsbin.cpp b/components/sceneutil/shadowsbin.cpp index abc1fa8b44..af62b581c9 100644 --- a/components/sceneutil/shadowsbin.cpp +++ b/components/sceneutil/shadowsbin.cpp @@ -54,6 +54,7 @@ ShadowsBin::ShadowsBin() mShaderAlphaTestStateSet = new osg::StateSet; mShaderAlphaTestStateSet->addUniform(new osg::Uniform("alphaTestShadows", true)); + 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) @@ -221,7 +222,7 @@ void ShadowsBin::sortImplementation() } if (!noTestRoot->_leaves.empty()) newList.push_back(noTestRoot); - _stateGraphList = newList; + _stateGraphList = std::move(newList); } } diff --git a/components/sceneutil/statesetupdater.cpp b/components/sceneutil/statesetupdater.cpp index 5d7dbd7559..a8232f938e 100644 --- a/components/sceneutil/statesetupdater.cpp +++ b/components/sceneutil/statesetupdater.cpp @@ -10,36 +10,57 @@ namespace SceneUtil void StateSetUpdater::operator()(osg::Node* node, osg::NodeVisitor* nv) { bool isCullVisitor = nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR; - if (!mStateSets[0]) + + if (isCullVisitor) + return applyCull(node, static_cast(nv)); + else + return applyUpdate(node, nv); + } + + void StateSetUpdater::applyUpdate(osg::Node* node, osg::NodeVisitor* nv) + { + if (!mStateSetsUpdate[0]) { - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - if (!isCullVisitor) - mStateSets[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults - else - mStateSets[i] = new osg::StateSet; - setDefaults(mStateSets[i]); + mStateSetsUpdate[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults + setDefaults(mStateSetsUpdate[i]); } } - osg::ref_ptr stateset = mStateSets[nv->getTraversalNumber()%2]; + osg::ref_ptr stateset = mStateSetsUpdate[nv->getTraversalNumber() % 2]; apply(stateset, nv); - - if (!isCullVisitor) - node->setStateSet(stateset); - else - static_cast(nv)->pushStateSet(stateset); - + node->setStateSet(stateset); traverse(node, nv); + } + + void StateSetUpdater::applyCull(osg::Node* node, osgUtil::CullVisitor* cv) + { + auto stateset = getCvDependentStateset(cv); + apply(stateset, cv); + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } - if (isCullVisitor) - static_cast(nv)->popStateSet(); + osg::StateSet* StateSetUpdater::getCvDependentStateset(osgUtil::CullVisitor* cv) + { + auto it = mStateSetsCull.find(cv); + if (it == mStateSetsCull.end()) + { + osg::ref_ptr stateset = new osg::StateSet; + mStateSetsCull.emplace(cv, stateset); + setDefaults(stateset); + return stateset; + } + return it->second; } void StateSetUpdater::reset() { - mStateSets[0] = nullptr; - mStateSets[1] = nullptr; + mStateSetsUpdate[0] = nullptr; + mStateSetsUpdate[1] = nullptr; + mStateSetsCull.clear(); } StateSetUpdater::StateSetUpdater() diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 25e50acfd2..263f76ae54 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -3,6 +3,14 @@ #include +#include +#include + +namespace osgUtil +{ + class CullVisitor; +} + namespace SceneUtil { @@ -11,11 +19,15 @@ namespace SceneUtil /// queues up a StateSet that we want to modify for the next frame. To solve this we could set the StateSet to /// DYNAMIC data variance but that would undo all the benefits of the threading model - having the cull and draw /// traversals run in parallel can yield up to 200% framerates. - /// @par Race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, + /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, + /// otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as an UpdateCallback, race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, /// one StateSet we can write to, the second one is currently in use by the draw traversal of the last frame. - /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as a CullCallback, race conditions are prevented by mapping statesets to cull visitors - OSG has two cull visitors that take turns, + /// allowing the updater to automatically scale for the number of views. + /// @note When used as a CullCallback, StateSetUpdater will have no effect on leaf nodes such as osg::Geometry and must be used on branch nodes only. /// @note Do not add the same StateSetUpdater to multiple nodes. - /// @note Do not add multiple StateSetControllers on the same Node as they will conflict - instead use the CompositeStateSetUpdater. + /// @note Do not add multiple StateSetUpdaters on the same Node as they will conflict - instead use the CompositeStateSetUpdater. class StateSetUpdater : public osg::NodeCallback { public: @@ -40,7 +52,12 @@ namespace SceneUtil void reset(); private: - osg::ref_ptr mStateSets[2]; + void applyCull(osg::Node* node, osgUtil::CullVisitor* cv); + void applyUpdate(osg::Node* node, osg::NodeVisitor* nv); + osg::StateSet* getCvDependentStateset(osgUtil::CullVisitor* cv); + + std::array, 2> mStateSetsUpdate; + std::map> mStateSetsCull; }; /// @brief A variant of the StateSetController that can be made up of multiple controllers all controlling the same target. diff --git a/components/sceneutil/util.cpp b/components/sceneutil/util.cpp index 9b4fb9d3fb..442b164981 100644 --- a/components/sceneutil/util.cpp +++ b/components/sceneutil/util.cpp @@ -4,15 +4,38 @@ #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 { @@ -150,6 +173,43 @@ void GlowUpdater::setDuration(float duration) mDuration = duration; } +// Allows camera to render to a color and floating point depth texture with a multisampled framebuffer. +// Must be set on a camera's cull callback. +class AttachMultisampledDepthColorCallback : public osg::NodeCallback +{ +public: + AttachMultisampledDepthColorCallback(osg::Texture2D* colorTex, osg::Texture2D* depthTex, int samples, int colorSamples) + { + int width = colorTex->getTextureWidth(); + int height = colorTex->getTextureHeight(); + + osg::ref_ptr rbColor = new osg::RenderBuffer(width, height, colorTex->getInternalFormat(), samples, colorSamples); + osg::ref_ptr rbDepth = new osg::RenderBuffer(width, height, depthTex->getInternalFormat(), samples, colorSamples); + + mMsaaFbo = new osg::FrameBufferObject; + mMsaaFbo->setAttachment(osg::Camera::COLOR_BUFFER0, osg::FrameBufferAttachment(rbColor)); + mMsaaFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(rbDepth)); + + mFbo = new osg::FrameBufferObject; + mFbo->setAttachment(osg::Camera::COLOR_BUFFER0, osg::FrameBufferAttachment(colorTex)); + mFbo->setAttachment(osg::Camera::DEPTH_BUFFER, osg::FrameBufferAttachment(depthTex)); + } + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::RenderStage* renderStage = static_cast(nv)->getCurrentRenderStage(); + + renderStage->setMultisampleResolveFramebufferObject(mFbo); + renderStage->setFrameBufferObject(mMsaaFbo); + + traverse(node, nv); + } + +private: + osg::ref_ptr mFbo; + osg::ref_ptr mMsaaFbo; +}; + void transformBoundingSphere (const osg::Matrixf& matrix, osg::BoundingSphere& bsphere) { osg::BoundingSphere::vec_type xdash = bsphere._center; @@ -285,4 +345,81 @@ 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 f297c42d2e..0bca32c325 100644 --- a/components/sceneutil/util.hpp +++ b/components/sceneutil/util.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -64,6 +65,31 @@ 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 1f5a0ea4cf..fde87c66ba 100644 --- a/components/sceneutil/visitor.cpp +++ b/components/sceneutil/visitor.cpp @@ -9,6 +9,9 @@ #include +#include +#include + namespace SceneUtil { @@ -24,7 +27,7 @@ namespace SceneUtil void FindByClassVisitor::apply(osg::Node &node) { - if (Misc::StringUtils::ciEqual(node.className(), mNameToFind)) + if (Misc::StringUtils::ciEqual(std::string_view(node.className()), mNameToFind)) mFoundNodes.push_back(&node); traverse(node); @@ -32,13 +35,13 @@ namespace SceneUtil void FindByNameVisitor::apply(osg::Group &group) { - if (!checkGroup(group)) + if (!mFoundNode && !checkGroup(group)) traverse(group); } void FindByNameVisitor::apply(osg::MatrixTransform &node) { - if (!checkGroup(node)) + if (!mFoundNode && !checkGroup(node)) traverse(node); } @@ -46,17 +49,6 @@ namespace SceneUtil { } - void DisableFreezeOnCullVisitor::apply(osg::MatrixTransform &node) - { - traverse(node); - } - - void DisableFreezeOnCullVisitor::apply(osg::Drawable& drw) - { - if (osgParticle::ParticleSystem* partsys = dynamic_cast(&drw)) - partsys->setFreezeOnCull(false); - } - void NodeMapVisitor::apply(osg::MatrixTransform& trans) { // Take transformation for first found node in file diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index 5e041dc454..fcf3c1f944 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -45,20 +45,6 @@ namespace SceneUtil std::vector mFoundNodes; }; - // Disable freezeOnCull for all visited particlesystems - class DisableFreezeOnCullVisitor : public osg::NodeVisitor - { - public: - DisableFreezeOnCullVisitor() - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - { - } - - void apply(osg::MatrixTransform& node) override; - - void apply(osg::Drawable& drw) override; - }; - /// Maps names to nodes class NodeMapVisitor : public osg::NodeVisitor { diff --git a/components/sceneutil/waterutil.cpp b/components/sceneutil/waterutil.cpp index 8a434105c8..ac171005ae 100644 --- a/components/sceneutil/waterutil.cpp +++ b/components/sceneutil/waterutil.cpp @@ -5,6 +5,8 @@ #include #include +#include "util.hpp" + namespace SceneUtil { // disable nonsense test against a worldsize bb what will always pass @@ -76,7 +78,7 @@ namespace SceneUtil stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - osg::ref_ptr depth (new osg::Depth); + auto depth = createDepth(); depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); diff --git a/components/sceneutil/workqueue.cpp b/components/sceneutil/workqueue.cpp index 3c1df80ac4..eb7a7b2cae 100644 --- a/components/sceneutil/workqueue.cpp +++ b/components/sceneutil/workqueue.cpp @@ -46,9 +46,12 @@ WorkQueue::~WorkQueue() void WorkQueue::start(std::size_t workerThreads) { + { + const std::lock_guard lock(mMutex); + mIsReleased = false; + } while (mThreads.size() < workerThreads) mThreads.emplace_back(std::make_unique(*this)); - mIsReleased = false; } void WorkQueue::stop() @@ -74,9 +77,9 @@ void WorkQueue::addWorkItem(osg::ref_ptr item, bool front) std::unique_lock lock(mMutex); if (front) - mQueue.push_front(item); + mQueue.push_front(std::move(item)); else - mQueue.push_back(item); + mQueue.push_back(std::move(item)); mCondition.notify_one(); } @@ -89,12 +92,11 @@ osg::ref_ptr WorkQueue::removeWorkItem() } if (!mQueue.empty()) { - osg::ref_ptr item = mQueue.front(); + osg::ref_ptr item = std::move(mQueue.front()); mQueue.pop_front(); return item; } - else - return nullptr; + return nullptr; } unsigned int WorkQueue::getNumItems() const diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index 56225868e3..626930291d 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -20,7 +20,7 @@ #include "imagetosurface.hpp" -#if defined(OSG_LIBRARY_STATIC) && !defined(ANDROID) +#if defined(OSG_LIBRARY_STATIC) && (!defined(ANDROID) || OSG_VERSION_GREATER_THAN(3, 6, 5)) // Sets the default windowing system interface according to the OS. // Necessary for OpenSceneGraph to do some things, like decompression. USE_GRAPHICSWINDOW() diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 9b057cdfc6..e057cfac02 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -17,7 +16,6 @@ namespace Shader { ShaderManager::ShaderManager() - : mLightingMethod(SceneUtil::LightingMethod::FFP) { } @@ -26,11 +24,6 @@ namespace Shader mPath = path; } - void ShaderManager::setLightingMethod(SceneUtil::LightingMethod method) - { - mLightingMethod = method; - } - bool addLineDirectivesAfterConditionalBlocks(std::string& source) { for (size_t position = 0; position < source.length(); ) @@ -345,19 +338,16 @@ namespace Shader return shaderIt->second; } - osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader) + osg::ref_ptr ShaderManager::getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate) { std::lock_guard lock(mMutex); ProgramMap::iterator found = mPrograms.find(std::make_pair(vertexShader, fragmentShader)); if (found == mPrograms.end()) { - osg::ref_ptr program (new osg::Program); + if (!programTemplate) programTemplate = mProgramTemplate; + osg::ref_ptr program = programTemplate ? static_cast(programTemplate->clone(osg::CopyOp::SHALLOW_COPY)) : new osg::Program; program->addShader(vertexShader); program->addShader(fragmentShader); - program->addBindAttribLocation("aOffset", 6); - program->addBindAttribLocation("aRotation", 7); - if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) - program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; @@ -371,11 +361,10 @@ namespace Shader void ShaderManager::setGlobalDefines(DefineMap & globalDefines) { mGlobalDefines = globalDefines; - for (auto shaderMapElement: mShaders) + for (const auto& [key, shader]: mShaders) { - std::string templateId = shaderMapElement.first.first; - ShaderManager::DefineMap defines = shaderMapElement.first.second; - osg::ref_ptr shader = shaderMapElement.second; + std::string templateId = key.first; + ShaderManager::DefineMap defines = key.second; if (shader == nullptr) // I'm not sure how to handle a shader that was already broken as there's no way to get a potential replacement to the nodes that need it. continue; @@ -391,13 +380,13 @@ namespace Shader void ShaderManager::releaseGLObjects(osg::State *state) { std::lock_guard lock(mMutex); - for (auto shader : mShaders) + for (const auto& [_, shader] : mShaders) { - if (shader.second != nullptr) - shader.second->releaseGLObjects(state); + if (shader != nullptr) + shader->releaseGLObjects(state); } - for (auto program : mPrograms) - program.second->releaseGLObjects(state); + for (const auto& [_, program] : mPrograms) + program->releaseGLObjects(state); } } diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 2450f0d6dc..d0ee069b10 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -8,29 +8,11 @@ #include #include - -#include - -#include - -namespace Resource -{ - class SceneManager; -} - -namespace SceneUtil -{ - enum class LightingMethod; -} +#include namespace Shader { - enum class UBOBinding - { - LightBuffer - }; - /// @brief Reads shader template files and turns them into a concrete shader, based on a list of define's. /// @par Shader templates can get the value of a define with the syntax @define. class ShaderManager @@ -41,8 +23,6 @@ namespace Shader void setShaderPath(const std::string& path); - void setLightingMethod(SceneUtil::LightingMethod method); - typedef std::map DefineMap; /// Create or retrieve a shader instance. @@ -53,7 +33,10 @@ namespace Shader /// @note Thread safe. osg::ref_ptr getShader(const std::string& templateName, const DefineMap& defines, osg::Shader::Type shaderType); - osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader); + osg::ref_ptr getProgram(osg::ref_ptr vertexShader, osg::ref_ptr fragmentShader, const osg::Program* programTemplate=nullptr); + + const osg::Program* getProgramTemplate() const { return mProgramTemplate; } + void setProgramTemplate(const osg::Program* program) { mProgramTemplate = program; } /// Get (a copy of) the DefineMap used to construct all shaders DefineMap getGlobalDefines(); @@ -81,9 +64,9 @@ namespace Shader typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; - SceneUtil::LightingMethod mLightingMethod; - std::mutex mMutex; + + osg::ref_ptr mProgramTemplate; }; bool parseFors(std::string& source, const std::string& templateName); diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 25dfa4af1e..cc2b781cfd 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -1,5 +1,8 @@ #include "shadervisitor.hpp" +#include +#include + #include #include #include @@ -56,7 +59,7 @@ namespace Shader bool hasUniform(const std::string& name) { return mUniforms.count(name); } bool hasMode(osg::StateAttribute::GLMode mode) { return mModes.count(mode); } - bool hasAttribute(osg::StateAttribute::TypeMemberPair typeMemberPair) { return mAttributes.count(typeMemberPair); } + bool hasAttribute(const osg::StateAttribute::TypeMemberPair &typeMemberPair) { return mAttributes.count(typeMemberPair); } bool hasAttribute(osg::StateAttribute::Type type, unsigned int member) { return hasAttribute(osg::StateAttribute::TypeMemberPair(type, member)); } const std::set& getAttributes() { return mAttributes; } @@ -113,7 +116,6 @@ namespace Shader , mImageManager(imageManager) , mDefaultShaderPrefix(defaultShaderPrefix) { - mRequirements.emplace_back(); } void ShaderVisitor::setForceShaders(bool force) @@ -340,15 +342,6 @@ namespace Shader mRequirements.back().mShaderRequired = true; } } - - if (diffuseMap) - { - if (!writableStateSet) - writableStateSet = getWritableStateSet(node); - // We probably shouldn't construct a new version of this each time as Uniforms use pointer comparison for early-out. - // Also it should probably belong to the shader manager or be applied by the shadows bin - writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", true)); - } } const osg::StateSet::AttributeList& attributes = stateset->getAttributeList(); @@ -427,7 +420,10 @@ namespace Shader void ShaderVisitor::pushRequirements(osg::Node& node) { - mRequirements.push_back(mRequirements.back()); + if (mRequirements.empty()) + mRequirements.emplace_back(); + else + mRequirements.push_back(mRequirements.back()); mRequirements.back().mNode = &node; } @@ -467,6 +463,9 @@ namespace Shader defineMap[texIt->second + std::string("UV")] = std::to_string(texIt->first); } + if (defineMap["diffuseMap"] == "0") + writableStateSet->addUniform(new osg::Uniform("useDiffuseMapForShadowAlpha", false)); + defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); @@ -559,7 +558,7 @@ namespace Shader if (vertexShader && fragmentShader) { - auto program = mShaderManager.getProgram(vertexShader, fragmentShader); + auto program = mShaderManager.getProgram(vertexShader, fragmentShader, mProgramTemplate); writableStateSet->setAttributeAndModes(program, osg::StateAttribute::ON); addedState->setAttributeAndModes(program); @@ -598,7 +597,7 @@ namespace Shader for (auto itr = writableStateSet->getUniformList().begin(); itr != writableStateSet->getUniformList().end();) { if (addedState->hasUniform(itr->first)) - writableStateSet->getUniformList().erase(itr); + writableStateSet->getUniformList().erase(itr++); else ++itr; } @@ -606,7 +605,7 @@ namespace Shader for (auto itr = writableStateSet->getModeList().begin(); itr != writableStateSet->getModeList().end();) { if (addedState->hasMode(itr->first)) - writableStateSet->getModeList().erase(itr); + writableStateSet->getModeList().erase(itr++); else ++itr; } diff --git a/components/shader/shadervisitor.hpp b/components/shader/shadervisitor.hpp index a5add473a6..3380a66cca 100644 --- a/components/shader/shadervisitor.hpp +++ b/components/shader/shadervisitor.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_SHADERVISITOR_H #include +#include namespace Resource { @@ -19,6 +20,8 @@ namespace Shader public: ShaderVisitor(ShaderManager& shaderManager, Resource::ImageManager& imageManager, const std::string& defaultShaderPrefix); + void setProgramTemplate(const osg::Program* programTemplate) { mProgramTemplate = programTemplate; } + /// By default, only bump mapped objects will have a shader added to them. /// Setting force = true will cause all objects to render using shaders, regardless of having a bump map. void setForceShaders(bool force); @@ -109,6 +112,8 @@ namespace Shader void createProgram(const ShaderRequirements& reqs); void ensureFFP(osg::Node& node); bool adjustGeometry(osg::Geometry& sourceGeometry, const ShaderRequirements& reqs); + + osg::ref_ptr mProgramTemplate; }; class ReinstateRemovedStateVisitor : public osg::NodeVisitor diff --git a/components/terrain/buffercache.cpp b/components/terrain/buffercache.cpp index f9eb7ae635..399df16d34 100644 --- a/components/terrain/buffercache.cpp +++ b/components/terrain/buffercache.cpp @@ -245,13 +245,13 @@ namespace Terrain { { std::lock_guard lock(mIndexBufferMutex); - for (auto indexbuffer : mIndexBufferMap) - indexbuffer.second->releaseGLObjects(state); + for (const auto& [_, indexbuffer] : mIndexBufferMap) + indexbuffer->releaseGLObjects(state); } { std::lock_guard lock(mUvBufferMutex); - for (auto uvbuffer : mUvBufferMap) - uvbuffer.second->releaseGLObjects(state); + for (const auto& [_, uvbuffer] : mUvBufferMap) + uvbuffer->releaseGLObjects(state); } } diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index 47c567f544..927530660e 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -3,28 +3,32 @@ #include #include #include -#include #include "world.hpp" #include "../esm/loadland.hpp" +#include +#include + namespace Terrain { -CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask): - mWorld(world), - mRoot(root), - mBorderMask(borderMask) +CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask, Resource::SceneManager* sceneManager) + : mWorld(world) + , mSceneManager(sceneManager) + , mRoot(root) + , mBorderMask(borderMask) { } -void CellBorder::createCellBorderGeometry(int x, int y) +osg::ref_ptr CellBorder::createBorderGeometry(float x, float y, float size, Terrain::Storage* terrain, Resource::SceneManager* sceneManager, int mask, + float offset, osg::Vec4f color) { const int cellSize = ESM::Land::REAL_SIZE; const int borderSegments = 40; - const float offset = 10.0; osg::Vec3 cellCorner = osg::Vec3(x * cellSize,y * cellSize,0); + size *= cellSize; osg::ref_ptr vertices = new osg::Vec3Array; osg::ref_ptr colors = new osg::Vec4Array; @@ -32,22 +36,22 @@ void CellBorder::createCellBorderGeometry(int x, int y) normals->push_back(osg::Vec3(0.0f,-1.0f, 0.0f)); - float borderStep = cellSize / ((float) borderSegments); + float borderStep = size / ((float)borderSegments); for (int i = 0; i <= 2 * borderSegments; ++i) { osg::Vec3f pos = i < borderSegments ? osg::Vec3(i * borderStep,0.0f,0.0f) : - osg::Vec3(cellSize,(i - borderSegments) * borderStep,0.0f); + osg::Vec3(size, (i - borderSegments) * borderStep,0.0f); pos += cellCorner; - pos += osg::Vec3f(0,0,mWorld->getHeightAt(pos) + offset); + pos += osg::Vec3f(0,0, terrain->getHeightAt(pos) + offset); vertices->push_back(pos); osg::Vec4f col = i % 2 == 0 ? osg::Vec4f(0,0,0,1) : - osg::Vec4f(1,1,0,1); + color; colors->push_back(col); } @@ -61,8 +65,8 @@ void CellBorder::createCellBorderGeometry(int x, int y) border->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP,0,vertices->size())); - osg::ref_ptr borderGeode = new osg::Geode; - borderGeode->addDrawable(border.get()); + osg::ref_ptr borderGeode = new osg::Group; + borderGeode->addChild(border.get()); osg::StateSet *stateSet = borderGeode->getOrCreateStateSet(); osg::ref_ptr material (new osg::Material); @@ -73,8 +77,15 @@ void CellBorder::createCellBorderGeometry(int x, int y) polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); - borderGeode->setNodeMask(mBorderMask); + sceneManager->recreateShaders(borderGeode, "debug"); + borderGeode->setNodeMask(mask); + return borderGeode; +} + +void CellBorder::createCellBorderGeometry(int x, int y) +{ + auto borderGeode = createBorderGeometry(x, y, 1.f, mWorld->getStorage(), mSceneManager, mBorderMask); mRoot->addChild(borderGeode); mCellBorderNodes[std::make_pair(x,y)] = borderGeode; diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index 908cdea097..4481816a55 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -4,8 +4,14 @@ #include #include +namespace Resource +{ + class SceneManager; +} + namespace Terrain { + class Storage; class World; /** @@ -16,7 +22,7 @@ namespace Terrain public: typedef std::map, osg::ref_ptr > CellGrid; - CellBorder(Terrain::World *world, osg::Group *root, int borderMask); + CellBorder(Terrain::World *world, osg::Group *root, int borderMask, Resource::SceneManager* sceneManager); void createCellBorderGeometry(int x, int y); void destroyCellBorderGeometry(int x, int y); @@ -26,8 +32,11 @@ namespace Terrain */ void destroyCellBorderGeometry(); + static osg::ref_ptr createBorderGeometry(float x, float y, float size, Storage* terrain, Resource::SceneManager* sceneManager, int mask, float offset = 10.0, osg::Vec4f color = { 1,1,0,0 }); + protected: Terrain::World *mWorld; + Resource::SceneManager* mSceneManager; osg::Group *mRoot; CellGrid mCellBorderNodes; diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index a744471de5..e040cbdd93 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include @@ -40,15 +39,30 @@ ChunkManager::ChunkManager(Storage *storage, Resource::SceneManager *sceneMgr, T mMultiPassRoot->setAttributeAndModes(material, osg::StateAttribute::ON); } +struct FindChunkTemplate +{ + void operator() (ChunkId id, osg::Object* obj) + { + if (std::get<0>(id) == std::get<0>(mId) && std::get<1>(id) == std::get<1>(mId)) + mFoundTemplate = obj; + } + ChunkId mId; + osg::ref_ptr mFoundTemplate; +}; + osg::ref_ptr ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { ChunkId id = std::make_tuple(center, lod, lodFlags); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) - return obj->asNode(); + return static_cast(obj.get()); else { - osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile); + FindChunkTemplate find; + find.mId = id; + mCache->call(find); + TerrainDrawable* templateGeometry = find.mFoundTemplate ? static_cast(find.mFoundTemplate.get()) : nullptr; + osg::ref_ptr node = createChunk(size, center, lod, lodFlags, compile, templateGeometry); mCache->addEntryToObjectCache(id, node.get()); return node; } @@ -166,24 +180,45 @@ std::vector > ChunkManager::createPasses(float chunk return ::Terrain::createPasses(useShaders, &mSceneManager->getShaderManager(), layers, blendmapTextures, blendmapScale, blendmapScale); } -osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile) +osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Vec2f &chunkCenter, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry) { - osg::ref_ptr positions (new osg::Vec3Array); - osg::ref_ptr normals (new osg::Vec3Array); - osg::ref_ptr colors (new osg::Vec4ubArray); - colors->setNormalize(true); - - osg::ref_ptr vbo (new osg::VertexBufferObject); - positions->setVertexBufferObject(vbo); - normals->setVertexBufferObject(vbo); - colors->setVertexBufferObject(vbo); - - mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); - osg::ref_ptr geometry (new TerrainDrawable); - geometry->setVertexArray(positions); - geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); - geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + + if (!templateGeometry) + { + osg::ref_ptr positions (new osg::Vec3Array); + osg::ref_ptr normals (new osg::Vec3Array); + osg::ref_ptr colors (new osg::Vec4ubArray); + colors->setNormalize(true); + + mStorage->fillVertexBuffers(lod, chunkSize, chunkCenter, positions, normals, colors); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + else + { + // Unfortunately we need to copy vertex data because of poor coupling with VertexBufferObject. + osg::ref_ptr positions = static_cast(templateGeometry->getVertexArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr normals = static_cast(templateGeometry->getNormalArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + osg::ref_ptr colors = static_cast(templateGeometry->getColorArray()->clone(osg::CopyOp::DEEP_COPY_ALL)); + + osg::ref_ptr vbo (new osg::VertexBufferObject); + positions->setVertexBufferObject(vbo); + normals->setVertexBufferObject(vbo); + colors->setVertexBufferObject(vbo); + + geometry->setVertexArray(positions); + geometry->setNormalArray(normals, osg::Array::BIND_PER_VERTEX); + geometry->setColorArray(colors, osg::Array::BIND_PER_VERTEX); + } + geometry->setUseDisplayList(false); geometry->setUseVertexBufferObjects(true); @@ -203,32 +238,44 @@ osg::ref_ptr ChunkManager::createChunk(float chunkSize, const osg::Ve geometry->setStateSet(mMultiPassRoot); - if (useCompositeMap) + if (templateGeometry) { - osg::ref_ptr compositeMap = new CompositeMap; - compositeMap->mTexture = createCompositeMapRTT(); - - createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); - - mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); - - geometry->setCompositeMap(compositeMap); - geometry->setCompositeMapRenderer(mCompositeMapRenderer); - - TextureLayer layer; - layer.mDiffuseMap = compositeMap->mTexture; - layer.mParallax = false; - layer.mSpecular = false; - geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), &mSceneManager->getShaderManager(), std::vector(1, layer), std::vector >(), 1.f, 1.f)); + if (templateGeometry->getCompositeMap()) + { + geometry->setCompositeMap(templateGeometry->getCompositeMap()); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); + } + geometry->setPasses(templateGeometry->getPasses()); } else { - geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + if (useCompositeMap) + { + osg::ref_ptr compositeMap = new CompositeMap; + compositeMap->mTexture = createCompositeMapRTT(); + + createCompositeMapGeometry(chunkSize, chunkCenter, osg::Vec4f(0,0,1,1), *compositeMap); + + mCompositeMapRenderer->addCompositeMap(compositeMap.get(), false); + + geometry->setCompositeMap(compositeMap); + geometry->setCompositeMapRenderer(mCompositeMapRenderer); + + TextureLayer layer; + layer.mDiffuseMap = compositeMap->mTexture; + layer.mParallax = false; + layer.mSpecular = false; + geometry->setPasses(::Terrain::createPasses(mSceneManager->getForceShaders() || !mSceneManager->getClampLighting(), &mSceneManager->getShaderManager(), std::vector(1, layer), std::vector >(), 1.f, 1.f)); + } + else + { + geometry->setPasses(createPasses(chunkSize, chunkCenter, false)); + } } geometry->setupWaterBoundingBox(-1, chunkSize * mStorage->getCellWorldSize() / numVerts); - if (compile && mSceneManager->getIncrementalCompileOperation()) + if (!templateGeometry && compile && mSceneManager->getIncrementalCompileOperation()) { mSceneManager->getIncrementalCompileOperation()->add(geometry); } diff --git a/components/terrain/chunkmanager.hpp b/components/terrain/chunkmanager.hpp index 9b7dbf3ee1..9b85e81330 100644 --- a/components/terrain/chunkmanager.hpp +++ b/components/terrain/chunkmanager.hpp @@ -26,6 +26,7 @@ namespace Terrain class CompositeMapRenderer; class Storage; class CompositeMap; + class TerrainDrawable; typedef std::tuple ChunkId; // Center, Lod, Lod Flags @@ -51,7 +52,7 @@ namespace Terrain void releaseGLObjects(osg::State* state) override; private: - osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile); + osg::ref_ptr createChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool compile, TerrainDrawable* templateGeometry); osg::ref_ptr createCompositeMapRTT(); diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index e662f4439f..22f507b3a2 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -105,9 +106,8 @@ namespace osg::ref_ptr mValue; LequalDepth() - : mValue(new osg::Depth) + : mValue(SceneUtil::createDepth()) { - mValue->setFunction(osg::Depth::LEQUAL); } }; @@ -166,6 +166,29 @@ namespace mValue->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); } }; + + class UniformCollection + { + public: + static const UniformCollection& value() + { + static UniformCollection instance; + return instance; + } + + osg::ref_ptr mDiffuseMap; + osg::ref_ptr mBlendMap; + osg::ref_ptr mNormalMap; + osg::ref_ptr mColorMode; + + UniformCollection() + : mDiffuseMap(new osg::Uniform("diffuseMap", 0)) + , mBlendMap(new osg::Uniform("blendMap", 1)) + , mNormalMap(new osg::Uniform("normalMap", 2)) + , mColorMode(new osg::Uniform("colorMode", 2)) + { + } + }; } namespace Terrain @@ -176,7 +199,6 @@ namespace Terrain std::vector > passes; unsigned int blendmapIndex = 0; - unsigned int passIndex = 0; for (std::vector::const_iterator it = layers.begin(); it != layers.end(); ++it) { bool firstLayer = (it == layers.begin()); @@ -186,7 +208,7 @@ namespace Terrain if (!blendmaps.empty()) { stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - stateset->setRenderBinDetails(passIndex++, "RenderBin"); + stateset->setRenderBinDetails(firstLayer ? 0 : 1, "RenderBin"); if (!firstLayer) { stateset->setAttributeAndModes(BlendFunc::value(), osg::StateAttribute::ON); @@ -199,32 +221,28 @@ namespace Terrain } } - int texunit = 0; - if (useShaders) { - stateset->setTextureAttributeAndModes(texunit, it->mDiffuseMap); + stateset->setTextureAttributeAndModes(0, it->mDiffuseMap); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - stateset->addUniform(new osg::Uniform("diffuseMap", texunit)); + stateset->addUniform(UniformCollection::value().mDiffuseMap); if (!blendmaps.empty()) { - ++texunit; osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->addUniform(new osg::Uniform("blendMap", texunit)); + stateset->setTextureAttributeAndModes(1, blendmap.get()); + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->addUniform(UniformCollection::value().mBlendMap); } if (it->mNormalMap) { - ++texunit; - stateset->setTextureAttributeAndModes(texunit, it->mNormalMap); - stateset->addUniform(new osg::Uniform("normalMap", texunit)); + stateset->setTextureAttributeAndModes(2, it->mNormalMap); + stateset->addUniform(UniformCollection::value().mNormalMap); } Shader::ShaderManager::DefineMap defineMap; @@ -242,31 +260,27 @@ namespace Terrain } stateset->setAttributeAndModes(shaderManager->getProgram(vertexShader, fragmentShader)); - stateset->addUniform(new osg::Uniform("colorMode", 2)); + stateset->addUniform(UniformCollection::value().mColorMode); } else { // Add the actual layer texture osg::ref_ptr tex = it->mDiffuseMap; - stateset->setTextureAttributeAndModes(texunit, tex.get()); + stateset->setTextureAttributeAndModes(0, tex.get()); if (layerTileSize != 1.f) - stateset->setTextureAttributeAndModes(texunit, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(0, LayerTexMat::value(layerTileSize), osg::StateAttribute::ON); // Multiply by the alpha map if (!blendmaps.empty()) { osg::ref_ptr blendmap = blendmaps.at(blendmapIndex++); - stateset->setTextureAttributeAndModes(texunit, blendmap.get()); + stateset->setTextureAttributeAndModes(1, blendmap.get()); // This is to map corner vertices directly to the center of a blendmap texel. - stateset->setTextureAttributeAndModes(texunit, BlendmapTexMat::value(blendmapScale)); - stateset->setTextureAttributeAndModes(texunit, TexEnvCombine::value(), osg::StateAttribute::ON); - - ++texunit; + stateset->setTextureAttributeAndModes(1, BlendmapTexMat::value(blendmapScale)); + stateset->setTextureAttributeAndModes(1, TexEnvCombine::value(), osg::StateAttribute::ON); } } diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index e9f5805566..4baf08149d 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "quadtreenode.hpp" #include "storage.hpp" @@ -53,11 +55,12 @@ namespace Terrain class DefaultLodCallback : public LodCallback { public: - DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid) + DefaultLodCallback(float factor, float minSize, float viewDistance, const osg::Vec4i& grid, float distanceModifier=0.f) : mFactor(factor) , mMinSize(minSize) , mViewDistance(viewDistance) , mActiveGrid(grid) + , mDistanceModifier(distanceModifier) { } @@ -65,8 +68,7 @@ public: { const osg::Vec2f& center = node->getCenter(); bool activeGrid = (center.x() > mActiveGrid.x() && center.y() > mActiveGrid.y() && center.x() < mActiveGrid.z() && center.y() < mActiveGrid.w()); - if (dist > mViewDistance && !activeGrid) // for Scene<->ObjectPaging sync the activegrid must remain loaded - return StopTraversal; + if (node->getSize()>1) { float halfSize = node->getSize()/2; @@ -77,6 +79,11 @@ public: 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))); @@ -88,6 +95,7 @@ private: float mMinSize; float mViewDistance; osg::Vec4i mActiveGrid; + float mDistanceModifier; }; class RootNode : public QuadTreeNode @@ -241,7 +249,27 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize) +class DebugChunkManager : public QuadTreeWorld::ChunkManager +{ +public: + DebugChunkManager(Resource::SceneManager* sceneManager, Storage* storage, unsigned int nodeMask) : mSceneManager(sceneManager), mStorage(storage), mNodeMask(nodeMask) {} + osg::ref_ptr getChunk(float size, const osg::Vec2f& chunkCenter, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) + { + osg::Vec3f center = { chunkCenter.x(), chunkCenter.y(), 0 }; + auto chunkBorder = CellBorder::createBorderGeometry(center.x() - size / 2.f, center.y() - size / 2.f, size, mStorage, mSceneManager, mNodeMask, 5.f, { 1, 0, 0, 0 }); + osg::ref_ptr trans = new osg::MatrixTransform(osg::Matrixf::translate(-center*Constants::CellSizeInUnits)); + trans->setDataVariance(osg::Object::STATIC); + trans->addChild(chunkBorder); + return trans; + } + unsigned int getNodeMask() { return mNodeMask; } +private: + Resource::SceneManager* mSceneManager; + Storage* mStorage; + unsigned int mNodeMask; +}; + +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float compMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks) : TerrainGrid(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) @@ -249,22 +277,18 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour , mVertexLodMod(vertexLodMod) , mViewDistance(std::numeric_limits::max()) , mMinSize(1/8.f) + , mDebugTerrainChunks(debugChunks) { mChunkManager->setCompositeMapSize(compMapResolution); mChunkManager->setCompositeMapLevel(compMapLevel); mChunkManager->setMaxCompositeGeometrySize(maxCompGeometrySize); mChunkManagers.push_back(mChunkManager.get()); -} -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize) - : TerrainGrid(parent, storage, nodeMask) - , mViewDataMap(new ViewDataMap) - , mQuadTreeBuilt(false) - , mLodFactor(lodFactor) - , mVertexLodMod(0) - , mViewDistance(std::numeric_limits::max()) - , mMinSize(chunkSize) -{ + if (mDebugTerrainChunks) + { + mDebugChunkManager = std::unique_ptr(new DebugChunkManager(mResourceSystem->getSceneManager(), mStorage, borderMask)); + addChunkManager(mDebugChunkManager.get()); + } } QuadTreeWorld::~QuadTreeWorld() @@ -322,7 +346,7 @@ unsigned int getLodFlags(QuadTreeNode* node, int ourLod, int vertexLodMod, const return lodFlags; } -void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile) +void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, float cellWorldSize, const osg::Vec4i &gridbounds, const std::vector& chunkManagers, bool compile, float reuseDistance) { if (!vd->hasChanged() && entry.mRenderingNode) return; @@ -350,6 +374,8 @@ void loadRenderingNode(ViewData::Entry& entry, ViewData* vd, int vertexLodMod, f for (QuadTreeWorld::ChunkManager* m : chunkManagers) { + 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); if (n) pat->addChild(n); } @@ -444,7 +470,7 @@ 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); + loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, mActiveGrid, mChunkManagers, false, mViewDataMap->getReuseDistance()); entry.mRenderingNode->accept(nv); } @@ -494,33 +520,46 @@ View* QuadTreeWorld::createView() return mViewDataMap->createIndependentView(); } -void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::Vec4i &grid, std::atomic &abort, std::atomic &progress, int& progressTotal) +void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::Vec4i &grid, std::atomic &abort, Loading::Reporter& reporter) { ensureQuadTreeBuilt(); + const float cellWorldSize = mStorage->getCellWorldSize(); ViewData* vd = static_cast(view); vd->setViewPoint(viewPoint); vd->setActiveGrid(grid); - DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid); - mRootNode->traverseNodes(vd, viewPoint, &lodCallback); - if (!progressTotal) - for (unsigned int i=0; igetNumEntries(); ++i) - progressTotal += vd->getEntry(i).mNode->getSize(); - - const float cellWorldSize = mStorage->getCellWorldSize(); - for (unsigned int i=0; igetNumEntries() && !abort; ++i) + for (unsigned int pass=0; pass<3; ++pass) { - ViewData::Entry& entry = vd->getEntry(i); - loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true); - progress += entry.mNode->getSize(); - } - vd->markUnchanged(); -} + unsigned int startEntry = vd->getNumEntries(); -bool QuadTreeWorld::storeView(const View* view, double referenceTime) -{ - return mViewDataMap->storeView(static_cast(view), referenceTime); + float distanceModifier=0.f; + if (pass == 1) + distanceModifier = 1024; + else if (pass == 2) + distanceModifier = -1024; + DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid, distanceModifier); + mRootNode->traverseNodes(vd, viewPoint, &lodCallback); + + if (pass==0) + { + std::size_t progressTotal = 0; + for (unsigned int i = 0, n = vd->getNumEntries(); i < n; ++i) + progressTotal += vd->getEntry(i).mNode->getSize(); + + reporter.addTotal(progressTotal); + } + + const float reuseDistance = std::max(mViewDataMap->getReuseDistance(), std::abs(distanceModifier)); + for (unsigned int i=startEntry; igetNumEntries() && !abort; ++i) + { + ViewData::Entry& entry = vd->getEntry(i); + + loadRenderingNode(entry, vd, mVertexLodMod, cellWorldSize, grid, mChunkManagers, true, reuseDistance); + if (pass==0) reporter.addProgress(entry.mNode->getSize()); + entry.mNode = nullptr; // Clear node lest we break the neighbours search for the next pass + } + } } void QuadTreeWorld::reportStats(unsigned int frameNumber, osg::Stats *stats) diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index 2ddea42049..3bd606d6c6 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -5,6 +5,7 @@ #include "terraingrid.hpp" #include +#include namespace osg { @@ -15,14 +16,13 @@ namespace Terrain { class RootNode; class ViewDataMap; + class DebugChunkManager; /// @brief Terrain implementation that loads cells into a Quad Tree, with geometry LOD and texture LOD. class QuadTreeWorld : public TerrainGrid // note: derived from TerrainGrid is only to render default cells (see loadCell) { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize); - - QuadTreeWorld(osg::Group *parent, Storage *storage, unsigned int nodeMask, float lodFactor, float chunkSize); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, unsigned int nodeMask, unsigned int preCompileMask, unsigned int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize, bool debugChunks); ~QuadTreeWorld(); @@ -39,8 +39,7 @@ namespace Terrain void unloadCell(int x, int y) override; View* createView() override; - void preload(View* view, const osg::Vec3f& eyePoint, const osg::Vec4i &cellgrid, std::atomic& abort, std::atomic& progress, int& progressRange) override; - bool storeView(const View* view, double referenceTime) override; + void preload(View* view, const osg::Vec3f& eyePoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) override; void rebuildViews() override; void reportStats(unsigned int frameNumber, osg::Stats* stats) override; @@ -51,6 +50,11 @@ namespace Terrain virtual ~ChunkManager(){} virtual osg::ref_ptr getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) = 0; virtual unsigned int getNodeMask() { return 0; } + + void setViewDistance(float viewDistance) { mViewDistance = viewDistance; } + float getViewDistance() const { return mViewDistance; } + private: + float mViewDistance = 0.f; }; void addChunkManager(ChunkManager*); @@ -69,6 +73,8 @@ namespace Terrain int mVertexLodMod; float mViewDistance; float mMinSize; + bool mDebugTerrainChunks; + std::unique_ptr mDebugChunkManager; }; } diff --git a/components/terrain/terraindrawable.cpp b/components/terrain/terraindrawable.cpp index 746534abb4..231b6f4fed 100644 --- a/components/terrain/terraindrawable.cpp +++ b/components/terrain/terraindrawable.cpp @@ -94,10 +94,10 @@ void TerrainDrawable::cull(osgUtil::CullVisitor *cv) return; } - if (mCompositeMap) + if (mCompositeMap && mCompositeMapRenderer) { mCompositeMapRenderer->setImmediate(mCompositeMap); - mCompositeMap = nullptr; + mCompositeMapRenderer = nullptr; } bool pushedLight = mLightListCallback && mLightListCallback->pushLightState(this, cv); diff --git a/components/terrain/terraindrawable.hpp b/components/terrain/terraindrawable.hpp index dbfdd3c80a..721abe7481 100644 --- a/components/terrain/terraindrawable.hpp +++ b/components/terrain/terraindrawable.hpp @@ -45,6 +45,7 @@ namespace Terrain typedef std::vector > PassVector; void setPasses (const PassVector& passes); + const PassVector& getPasses() const { return mPasses; } void setLightListCallback(SceneUtil::LightListCallback* lightListCallback); @@ -56,6 +57,7 @@ namespace Terrain const osg::BoundingBox& getWaterBoundingBox() const { return mWaterBoundingBox; } void setCompositeMap(CompositeMap* map) { mCompositeMap = map; } + CompositeMap* getCompositeMap() { return mCompositeMap; } void setCompositeMapRenderer(CompositeMapRenderer* renderer) { mCompositeMapRenderer = renderer; } private: diff --git a/components/terrain/viewdata.cpp b/components/terrain/viewdata.cpp index 996bf909c8..c5070fdf0d 100644 --- a/components/terrain/viewdata.cpp +++ b/components/terrain/viewdata.cpp @@ -141,9 +141,9 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo vd = found->second; needsUpdate = false; - if (!vd->suitableToUse(activeGrid) || (vd->getViewPoint()-viewPoint).length2() >= mReuseDistance*mReuseDistance || vd->getWorldUpdateRevision() < mWorldUpdateRevision) + if (!(vd->suitableToUse(activeGrid) && (vd->getViewPoint()-viewPoint).length2() < mReuseDistance*mReuseDistance && vd->getWorldUpdateRevision() >= mWorldUpdateRevision)) { - float shortestDist = viewer ? mReuseDistance*mReuseDistance : std::numeric_limits::max(); + float shortestDist = mReuseDistance*mReuseDistance; const ViewData* mostSuitableView = nullptr; for (const ViewData* other : mUsedViews) { @@ -157,36 +157,22 @@ ViewData *ViewDataMap::getViewData(osg::Object *viewer, const osg::Vec3f& viewPo } } } - if (mostSuitableView && mostSuitableView != vd) + if (mostSuitableView) { vd->copyFrom(*mostSuitableView); return vd; } - else if (!mostSuitableView) + else { vd->setViewPoint(viewPoint); + vd->setActiveGrid(activeGrid); + vd->setWorldUpdateRevision(mWorldUpdateRevision); needsUpdate = true; } } - if (!vd->suitableToUse(activeGrid)) - { - vd->setViewPoint(viewPoint); - vd->setActiveGrid(activeGrid); - needsUpdate = true; - } return vd; } -bool ViewDataMap::storeView(const ViewData* view, double referenceTime) -{ - if (view->getWorldUpdateRevision() < mWorldUpdateRevision) - return false; - ViewData* store = createOrReuseView(); - store->copyFrom(*view); - store->setLastUsageTimeStamp(referenceTime); - return true; -} - ViewData *ViewDataMap::createOrReuseView() { ViewData* vd = nullptr; diff --git a/components/terrain/viewdata.hpp b/components/terrain/viewdata.hpp index 0289352585..5d814251ea 100644 --- a/components/terrain/viewdata.hpp +++ b/components/terrain/viewdata.hpp @@ -93,7 +93,8 @@ namespace Terrain void clearUnusedViews(double referenceTime); void rebuildViews(); - bool storeView(const ViewData* view, double referenceTime); + + float getReuseDistance() const { return mReuseDistance; } private: std::list mViewVector; diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index d1581724e0..17a51913e5 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "storage.hpp" #include "texturemanager.hpp" @@ -43,7 +44,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); mChunkManager->setNodeMask(nodeMask); - mCellBorder.reset(new CellBorder(this,mTerrainRoot.get(),borderMask)); + mCellBorder.reset(new CellBorder(this,mTerrainRoot.get(),borderMask,mResourceSystem->getSceneManager())); mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 5797d894ef..8d60e4c5b6 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -32,6 +32,11 @@ namespace SceneUtil class WorkQueue; } +namespace Loading +{ + class Reporter; +} + namespace Terrain { class Storage; @@ -148,11 +153,7 @@ namespace Terrain /// @note Thread safe, as long as you do not attempt to load into the same view from multiple threads. - virtual void preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i &cellgrid, std::atomic& abort, std::atomic& progress, int& progressRange) {} - - /// Store a preloaded view into the cache with the intent that the next rendering traversal can use it. - /// @note Not thread safe. - virtual bool storeView(const View* view, double referenceTime) {return true;} + virtual void preload(View* view, const osg::Vec3f& viewPoint, const osg::Vec4i &cellgrid, std::atomic& abort, Loading::Reporter& reporter) {} virtual void rebuildViews() {} diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index 045fe3cf5c..faebc782aa 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -86,14 +86,11 @@ namespace VFS return mIndex.find(normalized) != mIndex.end(); } - const std::map& Manager::getIndex() const + std::string Manager::normalizeFilename(const std::string& name) const { - return mIndex; - } - - void Manager::normalizeFilename(std::string &name) const - { - normalize_path(name, mStrict); + std::string result = name; + normalize_path(result, mStrict); + return result; } std::string Manager::getArchive(const std::string& name) const @@ -107,4 +104,24 @@ namespace VFS } return {}; } + + namespace + { + bool startsWith(std::string_view text, std::string_view start) + { + return text.rfind(start, 0) == 0; + } + } + + Manager::RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(const std::string& path) const + { + if (path.empty()) + return { mIndex.begin(), mIndex.end() }; + auto normalized = normalizeFilename(path); + const auto it = mIndex.lower_bound(normalized); + if (it == mIndex.end() || !startsWith(it->first, normalized)) + return { it, it }; + ++normalized.back(); + return { it, mIndex.lower_bound(normalized) }; + } } diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index 5a09a995eb..8568e8e784 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -12,6 +12,19 @@ namespace VFS class Archive; class File; + template + class IteratorPair + { + public: + IteratorPair(Iterator first, Iterator last) : mFirst(first), mLast(last) {} + Iterator begin() const { return mFirst; } + Iterator end() const { return mLast; } + + private: + Iterator mFirst; + Iterator mLast; + }; + /// @brief The main class responsible for loading files from a virtual file system. /// @par Various archive types (e.g. directories on the filesystem, or compressed archives) /// can be registered, and will be merged into a single file tree. If the same filename is @@ -19,6 +32,21 @@ namespace VFS /// @par Most of the methods in this class are considered thread-safe, see each method documentation for details. class Manager { + class RecursiveDirectoryIterator + { + public: + RecursiveDirectoryIterator(std::map::const_iterator it) : mIt(it) {} + const std::string& operator*() const { return mIt->first; } + const std::string* operator->() const { return &mIt->first; } + bool operator!=(const RecursiveDirectoryIterator& other) { return mIt != other.mIt; } + RecursiveDirectoryIterator& operator++() { ++mIt; return *this; } + + private: + std::map::const_iterator mIt; + }; + + using RecursiveDirectoryRange = IteratorPair; + public: /// @param strict Use strict path handling? If enabled, no case folding will /// be done, but slash/backslash conversions are always done. @@ -40,13 +68,9 @@ namespace VFS /// @note May be called from any thread once the index has been built. bool exists(const std::string& name) const; - /// Get a complete list of files from all archives - /// @note May be called from any thread once the index has been built. - const std::map& getIndex() const; - /// Normalize the given filename, making slashes/backslashes consistent, and lower-casing if mStrict is false. /// @note May be called from any thread once the index has been built. - void normalizeFilename(std::string& name) const; + [[nodiscard]] std::string normalizeFilename(const std::string& name) const; /// Retrieve a file by name. /// @note Throws an exception if the file can not be found. @@ -59,6 +83,13 @@ namespace VFS Files::IStreamPtr getNormalized(const std::string& normalizedName) const; std::string getArchive(const std::string& name) const; + + /// Recursivly iterate over the elements of the given path + /// In practice it return all files of the VFS starting with the given path + /// @note the path is normalized + /// @note May be called from any thread once the index has been built. + RecursiveDirectoryRange getRecursiveDirectoryIterator(const std::string& path) const; + private: bool mStrict; diff --git a/components/widgets/sharedstatebutton.cpp b/components/widgets/sharedstatebutton.cpp index f4456275bd..91436a1b96 100644 --- a/components/widgets/sharedstatebutton.cpp +++ b/components/widgets/sharedstatebutton.cpp @@ -18,7 +18,7 @@ namespace Gui } } - void SharedStateButton::shareStateWith(ButtonGroup shared) + void SharedStateButton::shareStateWith(const ButtonGroup &shared) { mSharedWith = shared; } diff --git a/components/widgets/sharedstatebutton.hpp b/components/widgets/sharedstatebutton.hpp index 42c6424b2c..d643bf2430 100644 --- a/components/widgets/sharedstatebutton.hpp +++ b/components/widgets/sharedstatebutton.hpp @@ -34,7 +34,7 @@ namespace Gui bool _setState(const std::string &_value); public: - void shareStateWith(ButtonGroup shared); + void shareStateWith(const ButtonGroup &shared); /// @note The ButtonGroup connection will be destroyed when any widget in the group gets destroyed. static void createButtonGroup(ButtonGroup group); diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index fab00ab9b7..362637746d 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -18,7 +18,7 @@ and run the install package once downloaded. It's now installed! The (bleeding edge) Source Way ============================== -Visit the `Development Environment Setup `_ +Visit the `Development Environment Setup `_ section of the Wiki for detailed instructions on how to build the engine. The Ubuntu Way diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 3a18f2445a..5075f8a5fe 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -7,12 +7,14 @@ Lua API reference engine_handlers openmw_util + openmw_settings openmw_core openmw_async openmw_query openmw_world openmw_self openmw_nearby + openmw_input openmw_ui openmw_aux_util @@ -37,6 +39,9 @@ Player scripts are local scripts that are attached to a player. |:ref:`openmw.util ` | everywhere | | Defines utility functions and classes like 3D vectors, | | | | | that don't depend on the game world. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.settings ` | everywhere | | Access to GMST records in content files (implemented) and | +| | | | to mod settings (not implemented). | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.core ` | everywhere | | Functions that are common for both global and local scripts | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.async ` | everywhere | | Timers (implemented) and coroutine utils (not implemented) | @@ -49,6 +54,8 @@ 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.ui ` | by player scripts | | Controls user interface | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |openmw.camera | by player scripts | | Controls camera (not implemented) | diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index f08892be36..bcbee4349e 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -36,8 +36,21 @@ Engine handler is a function defined by a script, that can be called by the engi +----------------------------------+----------------------------------------------------------------------+ | **Only for local scripts attached to a player** | +----------------------------------+----------------------------------------------------------------------+ -| onKeyPress(key) | | `Key `_ pressed. Usage example: | -| | | ``if key.symbol == 'z' and key.withShift then ...`` | +| onKeyPress(key) | | `Key `_ is pressed. | +| | | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` | ++----------------------------------+----------------------------------------------------------------------+ +| onKeyRelease(key) | | `Key `_ is released. | +| | | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` | ++----------------------------------+----------------------------------------------------------------------+ +| onControllerButtonPress(id) | | A `button `_ on a game | +| | controller is pressed. Usage example: | +| | | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` | ++----------------------------------+----------------------------------------------------------------------+ +| onControllerButtonRelease(id) | | A `button `_ on a game | +| | controller is released. Usage example: | +| | | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` | ++----------------------------------+----------------------------------------------------------------------+ +| onInputAction(id) | | `Game control `_ is pressed. | +| | | Usage example: ``if id == input.ACTION.ToggleWeapon then ...`` | +----------------------------------+----------------------------------------------------------------------+ - diff --git a/docs/source/reference/lua-scripting/openmw_input.rst b/docs/source/reference/lua-scripting/openmw_input.rst new file mode 100644 index 0000000000..39136dc4c2 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_input.rst @@ -0,0 +1,6 @@ +Package openmw.input +==================== + +.. raw:: html + :file: generated_html/openmw_input.html + diff --git a/docs/source/reference/lua-scripting/openmw_settings.rst b/docs/source/reference/lua-scripting/openmw_settings.rst new file mode 100644 index 0000000000..f3d26882bb --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_settings.rst @@ -0,0 +1,5 @@ +Package openmw.settings +======================= + +.. raw:: html + :file: generated_html/openmw_settings.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index d3ce319d01..44c79c35d8 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -299,6 +299,9 @@ Player scripts are local scripts that are attached to a player. |:ref:`openmw.util ` | everywhere | | Defines utility functions and classes like 3D vectors, | | | | | that don't depend on the game world. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.settings ` | everywhere | | Access to GMST records in content files (implemented) and | +| | | | to mod settings (not implemented). | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.core ` | everywhere | | Functions that are common for both global and local scripts | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.async ` | everywhere | | Timers (implemented) and coroutine utils (not implemented) | @@ -311,6 +314,8 @@ 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.ui ` | by player scripts | | Controls user interface | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |openmw.camera | by player scripts | | Controls camera (not implemented) | diff --git a/docs/source/reference/modding/paths.rst b/docs/source/reference/modding/paths.rst index 97cfe37a5c..db5000527b 100644 --- a/docs/source/reference/modding/paths.rst +++ b/docs/source/reference/modding/paths.rst @@ -29,7 +29,7 @@ Savegames +--------------+-----------------------------------------------------------------------------------------------------+ | OS | Location | +==============+=====================================================================================================+ -| Linux | ``$HOME/.config/openmw/saves`` | +| Linux | ``$HOME/.local/share/openmw/saves`` | +--------------+-----------------------------------------------------------------------------------------------------+ | Mac | ``$HOME/Library/Application\ Support/openmw/saves`` | +--------------+---------------+-------------------------------------------------------------------------------------+ diff --git a/docs/source/reference/modding/settings/camera.rst b/docs/source/reference/modding/settings/camera.rst index a880a2b62d..fe6217b22b 100644 --- a/docs/source/reference/modding/settings/camera.rst +++ b/docs/source/reference/modding/settings/camera.rst @@ -5,8 +5,8 @@ near clip --------- :Type: floating point -:Range: > 0 -:Default: 3.0 +:Range: >= 0.005 +:Default: 1.0 This setting controls the distance to the near clipping plane. The value must be greater than zero. Values greater than approximately 18.0 will occasionally clip objects in the world in front of the character. @@ -235,3 +235,23 @@ Maximum roll angle in degrees. This setting can only be configured by editing the settings configuration file. +reverse z +--------- + +:Type: boolean +:Range: True/False +:Default: True + +Enables a reverse-z depth buffer in which the depth range is reversed. This +allows for small :ref:`near clip` values and removes almost all z-fighting with +terrain and even tightly coupled meshes at extreme view distances. For this to +be useful, a floating point depth buffer is required. These features require +driver and hardware support, but should work on any semi-modern desktop hardware +through OpenGL extensions. The exception is macOS, which has since dropped +development of OpenGL drivers. If unsupported, this setting has no effect. + +Note, this will force OpenMW to use shaders as if :ref:`force shaders` was enabled. +The performance impact of this feature should be negligible. + +This setting can only be configured by editing the settings configuration file. + diff --git a/docs/source/reference/modding/settings/groundcover.rst b/docs/source/reference/modding/settings/groundcover.rst index 7c5a965e01..3e943e4284 100644 --- a/docs/source/reference/modding/settings/groundcover.rst +++ b/docs/source/reference/modding/settings/groundcover.rst @@ -40,20 +40,6 @@ May affect performance a lot. This setting can only be configured by editing the settings configuration file. -min chunk size --------------- - -:Type: floating point -:Range: 0.125, 0.25, 0.5, 1.0 -:Default: 0.5 - -Determines a minimum size of groundcover chunks in cells. For example, with 0.5 value -chunks near player will have size 4096x4096 game units. Larger chunks reduce CPU usage -(Draw and Cull bars), but can increase GPU usage (GPU bar) since culling becomes less efficient. -Smaller values do an opposite. - -This setting can only be configured by editing the settings configuration file. - stomp mode ---------- diff --git a/docs/source/reference/modding/settings/map.rst b/docs/source/reference/modding/settings/map.rst index ac989f3dea..a4d3cd7e0d 100644 --- a/docs/source/reference/modding/settings/map.rst +++ b/docs/source/reference/modding/settings/map.rst @@ -103,3 +103,30 @@ and typically require more panning to see all available portions of the map. This larger size also enables an overall greater level of detail if the local map resolution setting is also increased. This setting can not be configured except by editing the settings configuration file. + +allow zooming +------------- + +:Type: boolean +:Range: True/False +:Default: False + +If this setting is true the user can zoom in/out on local and global map with the mouse wheel. + +This setting can be controlled in Advanced tab of the launcher. + +max local viewing distance +--------------------------- + +:Type: integer +:Range: > 0 +:Default: 10 + +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. + +This setting can not be configured except by editing the settings configuration file. diff --git a/docs/source/reference/modding/settings/physics.rst b/docs/source/reference/modding/settings/physics.rst index 59aba91aa9..c6d27f58f8 100644 --- a/docs/source/reference/modding/settings/physics.rst +++ b/docs/source/reference/modding/settings/physics.rst @@ -6,7 +6,7 @@ async num threads :Type: integer :Range: >= 0 -:Default: 0 +:Default: 1 Determines how many threads will be spawned to compute physics update in the background (that is, process actors movement). A value of 0 means that the update will be performed in the main thread. A value greater than 1 requires the Bullet library be compiled with multithreading support. If that's not the case, a warning will be written in ``openmw.log`` and a value of 1 will be used. @@ -19,19 +19,9 @@ lineofsight keep inactive cache :Default: 0 The line of sight determines if 2 actors can see each other (without taking into account game mechanics such as invisibility or sneaking). It is used by some scripts (the getLOS function), by the AI (to determine if an actor should start combat or chase an opponent) and for functionnalities such as greetings or turning NPC head toward an object. -This parameters determine for how long a cache of request should be kept warm. It depends on :ref:`async num threads` being > 0, otherwise a value of -1 will be used. If a request is not found in the cache, it is always fulfilled immediately. In case Bullet is compiled without multithreading support, non-cached requests involve blocking the async thread(s), which might hurt performance. -A value of -1 means no caching. -A value of 0 means that for as long as a request is made (after the first one), it will be preemptively "refreshed" in the async thread, without blocking neither the main thread nor the async thread. +This parameters determine for how long a cache of request should be kept warm. +A value of 0 means that the cache is kept only for the current frame, that is if a request is done 2 times in the same frame, the second request will be in cache. Any value > 0 is the number of frames for which the values are kept in cache even if the results was not requested again. -If Bullet is compiled with multithreading support, requests are non blocking, it is better to set this parameter to -1. - -defer aabb update ------------------ - -:Type: boolean -:Range: True/False -:Default: True - -Axis-aligned bounding box (aabb for short) are used by Bullet for collision detection. They should be updated anytime a physical object is modified (for instance moved) for collision detection to be correct. -This parameter control wether the update should be done as soon as the object is modified (the default), which involves blocking the async thread(s), or queue the modifications to update them as a batch before the collision detections. It depends on :ref:`async num threads` being > 0, otherwise it will be disabled. -Disabling this parameter is intended as an aid for debugging collisions detection issues. +If :ref:`async num threads` is 0, a value of 0 will be used. +If a request is not found in the cache, it is always fulfilled immediately. In case Bullet is compiled without multithreading support, non-cached requests involve blocking the async thread, which might hurt performance. +If Bullet is compiled with multithreading support, requests are non blocking, it is better to set this parameter to 0. diff --git a/docs/source/reference/modding/settings/terrain.rst b/docs/source/reference/modding/settings/terrain.rst index e6018a8655..752260fd48 100644 --- a/docs/source/reference/modding/settings/terrain.rst +++ b/docs/source/reference/modding/settings/terrain.rst @@ -100,6 +100,18 @@ max composite geometry size Controls the maximum size of simple composite geometry chunk in cell units. With small values there will more draw calls and small textures, but higher values create more overdraw (not every texture layer is used everywhere). +debug chunks +------------ + +:Type: boolean +:Range: True/False +:Default: False + +This debug setting allows you to see the borders of each chunks of the world by drawing lines arround them (as with toggleborder). +If object paging is set to true then this debug setting will allows you to see what objects have been merged in the scene +by making them colored randomly. + + object paging ------------- @@ -194,12 +206,3 @@ object paging min size cost multiplier This setting adjusts the calculated cost of merging an object used in the mentioned functionality. The larger this value is, the less expensive objects can be before they are discarded. See the formula above to figure out the math. - -object paging debug batches ---------------------------- -:Type: boolean -:Range: True/False -:Default: False - -This debug setting allows you to see what objects have been merged in the scene -by making them colored randomly. diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 5dc9642d7c..6ed0bffa6b 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -1,5 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later +set(CMAKE_CXX_CLANG_TIDY "") + # Like `FetchContent_MakeAvailable` but passes EXCLUDE_FROM_ALL to `add_subdirectory`. macro(FetchContent_MakeAvailableExcludeFromAll) foreach(contentName IN ITEMS ${ARGV}) diff --git a/extern/sol3.2.2/README.md b/extern/sol3.2.2/README.md new file mode 100644 index 0000000000..c831941822 --- /dev/null +++ b/extern/sol3.2.2/README.md @@ -0,0 +1,3 @@ +sol/sol.hpp is downloaded from https://github.com/ThePhD/sol2/releases/download/v3.2.2/sol.hpp + +License: MIT diff --git a/extern/sol3.2.2/sol/sol.hpp b/extern/sol3.2.2/sol/sol.hpp new file mode 100644 index 0000000000..c25fd549e7 --- /dev/null +++ b/extern/sol3.2.2/sol/sol.hpp @@ -0,0 +1,26674 @@ +// The MIT License (MIT) + +// Copyright (c) 2013-2020 Rapptz, ThePhD and contributors + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// This file was generated with a script. +// Generated 2020-10-03 21:34:24.496436 UTC +// This header was generated with sol v3.2.1 (revision 48eea7b5) +// https://github.com/ThePhD/sol2 + +#ifndef SOL_SINGLE_INCLUDE_HPP +#define SOL_SINGLE_INCLUDE_HPP + +// beginning of sol/sol.hpp + +#ifndef SOL_HPP +#define SOL_HPP + +// beginning of sol/version.hpp + +#include + +#include + +#define SOL_VERSION_MAJOR 3 +#define SOL_VERSION_MINOR 5 +#define SOL_VERSION_PATCH 0 +#define SOL_VERSION_STRING "3.5.0" +#define SOL_VERSION ((SOL_VERSION_MAJOR * 100000) + (SOL_VERSION_MINOR * 100) + (SOL_VERSION_PATCH)) + +#define SOL_IS_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) != 0) +#define SOL_IS_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3) == 0) +#define SOL_IS_DEFAULT_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) > 3) +#define SOL_IS_DEFAULT_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3 OP_SYMBOL 3) < 0) + +#define SOL_ON | +#define SOL_OFF ^ +#define SOL_DEFAULT_ON + +#define SOL_DEFAULT_OFF - + +#if defined(_MSC_VER) + #define SOL_COMPILER_CLANG_I_ SOL_OFF + #define SOL_COMPILER_GCC_I_ SOL_OFF + #define SOL_COMPILER_EDG_I_ SOL_OFF + #define SOL_COMPILER_VCXX_I_ SOL_ON +#elif defined(__clang__) + #define SOL_COMPILER_CLANG_I_ SOL_ON + #define SOL_COMPILER_GCC_I_ SOL_OFF + #define SOL_COMPILER_EDG_I_ SOL_OFF + #define SOL_COMPILER_VCXX_I_ SOL_OFF +#elif defined(__GNUC__) + #define SOL_COMPILER_CLANG_I_ SOL_OFF + #define SOL_COMPILER_GCC_I_ SOL_ON + #define SOL_COMPILER_EDG_I_ SOL_OFF + #define SOL_COMPILER_VCXX_I_ SOL_OFF +#else + #define SOL_COMPILER_CLANG_I_ SOL_OFF + #define SOL_COMPILER_GCC_I_ SOL_OFF + #define SOL_COMPILER_EDG_I_ SOL_OFF + #define SOL_COMPILER_VCXX_I_ SOL_OFF +#endif + +#if defined(__MINGW32__) + #define SOL_COMPILER_FRONTEND_MINGW_I_ SOL_ON +#else + #define SOL_COMPILER_FRONTEND_MINGW_I_ SOL_OFF +#endif + +#if SIZE_MAX <= 0xFFFFULL + #define SOL_PLATFORM_X16_I_ SOL_ON + #define SOL_PLATFORM_X86_I_ SOL_OFF + #define SOL_PLATFORM_X64_I_ SOL_OFF +#elif SIZE_MAX <= 0xFFFFFFFFULL + #define SOL_PLATFORM_X16_I_ SOL_OFF + #define SOL_PLATFORM_X86_I_ SOL_ON + #define SOL_PLATFORM_X64_I_ SOL_OFF +#else + #define SOL_PLATFORM_X16_I_ SOL_OFF + #define SOL_PLATFORM_X86_I_ SOL_OFF + #define SOL_PLATFORM_X64_I_ SOL_ON +#endif + +#define SOL_PLATFORM_ARM32_I_ SOL_OFF +#define SOL_PLATFORM_ARM64_I_ SOL_OFF + +#if defined(_WIN32) + #define SOL_PLATFORM_WINDOWS_I_ SOL_ON +#else + #define SOL_PLATFORM_WINDOWS_I_ SOL_OFF +#endif +#if defined(__APPLE__) + #define SOL_PLATFORM_APPLE_I_ SOL_ON +#else + #define SOL_PLATFORM_APPLE_I_ SOL_OFF +#endif +#if defined(__unix__) + #define SOL_PLATFORM_UNIXLIKE_I_ SOL_ON +#else + #define SOL_PLATFORM_UNIXLIKE_I_ SOL_OFF +#endif +#if defined(__linux__) + #define SOL_PLATFORM_LINUXLIKE_I_ SOL_ON +#else + #define SOL_PLATFORM_LINUXLIKE_I_ SOL_OFF +#endif + +#define SOL_PLATFORM_APPLE_IPHONE_I_ SOL_OFF +#define SOL_PLATFORM_BSDLIKE_I_ SOL_OFF + +#if defined(SOL_IN_DEBUG_DETECTED) + #if SOL_IN_DEBUG_DETECTED != 0 + #define SOL_DEBUG_BUILD_I_ SOL_ON + #else + #define SOL_DEBUG_BUILD_I_ SOL_OFF + #endif +#elif !defined(NDEBUG) + #if SOL_IS_ON(SOL_COMPILER_VCXX_I_) && defined(_DEBUG) + #define SOL_DEBUG_BUILD_I_ SOL_ON + #elif (SOL_IS_ON(SOL_COMPILER_CLANG_I_) || SOL_IS_ON(SOL_COMPILER_GCC_I_)) && !defined(__OPTIMIZE__) + #define SOL_DEBUG_BUILD_I_ SOL_ON + #else + #define SOL_DEBUG_BUILD_I_ SOL_OFF + #endif +#else + #define SOL_DEBUG_BUILD_I_ SOL_DEFAULT_OFF +#endif // We are in a debug mode of some sort + +#if defined(SOL_NO_EXCEPTIONS) + #if (SOL_NO_EXCEPTIONS != 0) + #define SOL_EXCEPTIONS_I_ SOL_OFF + #else + #define SOL_EXCEPTIONS_I_ SOL_ON + #endif +#elif SOL_IS_ON(SOL_COMPILER_VCXX_I_) + #if !defined(_CPPUNWIND) + #define SOL_EXCEPTIONS_I_ SOL_OFF + #else + #define SOL_EXCEPTIONS_I_ SOL_ON + #endif +#elif SOL_IS_ON(SOL_COMPILER_CLANG_I_) || SOL_IS_ON(SOL_COMPILER_GCC_I_) + #if !defined(__EXCEPTIONS) + #define SOL_EXCEPTIONS_I_ SOL_OFF + #else + #define SOL_EXCEPTIONS_I_ SOL_ON + #endif +#else + #define SOL_EXCEPTIONS_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_NO_RTTI) + #if (SOL_NO_RTTI != 0) + #define SOL_RTTI_I_ SOL_OFF + #else + #define SOL_RTTI_I_ SOL_ON + #endif +#elif SOL_IS_ON(SOL_COMPILER_VCXX_I_) + #if !defined(_CPPRTTI) + #define SOL_RTTI_I_ SOL_OFF + #else + #define SOL_RTTI_I_ SOL_ON + #endif +#elif SOL_IS_ON(SOL_COMPILER_CLANG_I_) || SOL_IS_ON(SOL_COMPILER_GCC_I_) + #if !defined(__GXX_RTTI) + #define SOL_RTTI_I_ SOL_OFF + #else + #define SOL_RTTI_I_ SOL_ON + #endif +#else + #define SOL_RTTI_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_NO_THREAD_LOCAL) && (SOL_NO_THREAD_LOCAL != 0) + #define SOL_USE_THREAD_LOCAL_I_ SOL_OFF +#else + #define SOL_USE_THREAD_LOCAL_I_ SOL_DEFAULT_ON +#endif // thread_local keyword is bjorked on some platforms + +#if defined(SOL_ALL_SAFETIES_ON) && (SOL_ALL_SAFETIES_ON != 0) + #define SOL_ALL_SAFETIES_ON_I_ SOL_ON +#else + #define SOL_ALL_SAFETIES_ON_I_ SOL_DEFAULT_OFF +#endif + +#if defined(SOL_SAFE_GETTER) && (SOL_SAFE_GETTER != 0) + #define SOL_SAFE_GETTER_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_GETTER_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_USERTYPE) && (SOL_SAFE_USERTYPE != 0) + #define SOL_SAFE_USERTYPE_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_USERTYPE_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_REFERENCES) && (SOL_SAFE_REFERENCES != 0) + #define SOL_SAFE_REFERENCES_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_REFERENCES_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if (defined(SOL_SAFE_FUNCTIONS) && (SOL_SAFE_FUNCTIONS != 0)) \ + || (defined(SOL_SAFE_FUNCTION_OBJECTS) && (SOL_SAFE_FUNCTION_OBJECTS != 0)) + #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_FUNCTION_CALLS) && (SOL_SAFE_FUNCTION_CALLS != 0) + #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_PROXIES) && (SOL_SAFE_PROXIES != 0) + #define SOL_SAFE_PROXIES_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_PROXIES_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_NUMERICS) && (SOL_SAFE_NUMERICS != 0) + #define SOL_SAFE_NUMERICS_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_NUMERICS_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_SAFE_STACK_CHECK) && (SOL_SAFE_STACK_CHECK != 0) + #define SOL_SAFE_STACK_CHECK_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_SAFE_STACK_CHECK_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_ON + #else + #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if (defined(SOL_NO_CHECK_NUMBER_PRECISION) && (SOL_NO_CHECK_NUMBER_PRECISION != 0)) \ + || (defined(SOL_NO_CHECKING_NUMBER_PRECISION) && (SOL_NO_CHECKING_NUMBER_PRECISION != 0)) + #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_OFF +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON + #elif SOL_IS_ON(SOL_SAFE_NUMERICS_I_) + #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_ON + #else + #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_STRINGS_ARE_NUMBERS) + #if (SOL_STRINGS_ARE_NUMBERS != 0) + #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_ON + #else + #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_OFF + #endif +#else + #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_DEFAULT_OFF +#endif + +#if defined(SOL_ENABLE_INTEROP) && (SOL_ENABLE_INTEROP != 0) \ + || defined(SOL_USE_INTEROP) && (SOL_USE_INTEROP != 0) + #define SOL_USE_INTEROP_I_ SOL_ON +#else + #define SOL_USE_INTEROP_I_ SOL_DEFAULT_OFF +#endif + +#if defined(SOL_NO_NIL) + #if (SOL_NO_NIL != 0) + #define SOL_NIL_I_ SOL_OFF + #else + #define SOL_NIL_I_ SOL_ON + #endif +#elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) || defined(__OBJC__) || defined(nil) + #define SOL_NIL_I_ SOL_DEFAULT_OFF +#else + #define SOL_NIL_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_USERTYPE_TYPE_BINDING_INFO) + #if (SOL_USERTYPE_TYPE_BINDING_INFO != 0) + #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_ON + #else + #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_OFF + #endif +#else + #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_DEFAULT_ON +#endif // We should generate a my_type.__type table with lots of class information for usertypes + +#if defined(SOL_AUTOMAGICAL_TYPES_BY_DEFAULT) + #if (SOL_AUTOMAGICAL_TYPES_BY_DEFAULT != 0) + #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON + #else + #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF + #endif +#elif defined(SOL_DEFAULT_AUTOMAGICAL_USERTYPES) + #if (SOL_DEFAULT_AUTOMAGICAL_USERTYPES != 0) + #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON + #else + #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF + #endif +#else + #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_DEFAULT_ON +#endif // make is_automagical on/off by default + +#if defined(SOL_STD_VARIANT) + #if (SOL_STD_VARIANT != 0) + #define SOL_STD_VARIANT_I_ SOL_ON + #else + #define SOL_STD_VARIANT_I_ SOL_OFF + #endif +#else + #if SOL_IS_ON(SOL_COMPILER_CLANG_I_) && SOL_IS_ON(SOL_PLATFORM_APPLE_I_) + #if defined(__has_include) + #if __has_include() + #define SOL_STD_VARIANT_I_ SOL_ON + #else + #define SOL_STD_VARIANT_I_ SOL_OFF + #endif + #else + #define SOL_STD_VARIANT_I_ SOL_OFF + #endif + #else + #define SOL_STD_VARIANT_I_ SOL_DEFAULT_ON + #endif +#endif // make is_automagical on/off by default + +#if defined(SOL_NOEXCEPT_FUNCTION_TYPE) + #if (SOL_NOEXCEPT_FUNCTION_TYPE != 0) + #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON + #else + #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF + #endif +#else + #if defined(__cpp_noexcept_function_type) + #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON + #elif SOL_IS_ON(SOL_COMPILER_VCXX_I_) && (defined(_MSVC_LANG) && (_MSVC_LANG < 201403L)) + // There is a bug in the VC++ compiler?? + // on /std:c++latest under x86 conditions (VS 15.5.2), + // compiler errors are tossed for noexcept markings being on function types + // that are identical in every other way to their non-noexcept marked types function types... + // VS 2019: There is absolutely a bug. + #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF + #else + #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_DEFAULT_ON + #endif +#endif // noexcept is part of a function's type + +#if defined(SOL_STACK_STRING_OPTIMIZATION_SIZE) && SOL_STACK_STRING_OPTIMIZATION_SIZE > 0 + #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ SOL_STACK_STRING_OPTIMIZATION_SIZE +#else + #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ 1024 +#endif + +#if defined(SOL_ID_SIZE) && SOL_ID_SIZE > 0 + #define SOL_ID_SIZE_I_ SOL_ID_SIZE +#else + #define SOL_ID_SIZE_I_ 512 +#endif + +#if defined(LUA_IDSIZE) && LUA_IDSIZE > 0 + #define SOL_FILE_ID_SIZE_I_ LUA_IDSIZE +#elif defined(SOL_ID_SIZE) && SOL_ID_SIZE > 0 + #define SOL_FILE_ID_SIZE_I_ SOL_FILE_ID_SIZE +#else + #define SOL_FILE_ID_SIZE_I_ 2048 +#endif + +#if defined(SOL_PRINT_ERRORS) + #if (SOL_PRINT_ERRORS != 0) + #define SOL_PRINT_ERRORS_I_ SOL_ON + #else + #define SOL_PRINT_ERRORS_I_ SOL_OFF + #endif +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_PRINT_ERRORS_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_PRINT_ERRORS_I_ SOL_DEFAULT_ON + #else + #define SOL_PRINT_ERRORS_I_ SOL_OFF + #endif +#endif + +#if defined(SOL_DEFAULT_PASS_ON_ERROR) && (SOL_DEFAULT_PASS_ON_ERROR != 0) + #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_ON +#else + #if SOL_IS_ON(SOL_ALL_SAFETIES_ON_I_) + #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_ON + #elif SOL_IS_ON(SOL_DEBUG_BUILD_I_) + #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_DEFAULT_ON + #else + #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_OFF + #endif +#endif + +#if defined(SOL_USING_CXX_LUA) + #if (SOL_USING_CXX_LUA != 0) + #define SOL_USE_CXX_LUA_I_ SOL_ON + #else + #define SOL_USE_CXX_LUA_I_ SOL_OFF + #endif +#elif defined(SOL_USE_CXX_LUA) + #if (SOL_USE_CXX_LUA != 0) + #define SOL_USE_CXX_LUA_I_ SOL_ON + #else + #define SOL_USE_CXX_LUA_I_ SOL_OFF + #endif +#else + #define SOL_USE_CXX_LUA_I_ SOL_OFF +#endif + +#if defined(SOL_USING_CXX_LUAJIT) + #if (SOL_USING_CXX_LUA != 0) + #define SOL_USE_CXX_LUAJIT_I_ SOL_ON + #else + #define SOL_USE_CXX_LUAJIT_I_ SOL_OFF + #endif +#elif defined(SOL_USE_CXX_LUAJIT) + #if (SOL_USE_CXX_LUA != 0) + #define SOL_USE_CXX_LUAJIT_I_ SOL_ON + #else + #define SOL_USE_CXX_LUAJIT_I_ SOL_OFF + #endif +#else + #define SOL_USE_CXX_LUAJIT_I_ SOL_OFF +#endif + +#if defined(SOL_NO_LUA_HPP) + #if (SOL_NO_LUA_HPP != 0) + #define SOL_USE_LUA_HPP_I_ SOL_OFF + #else + #define SOL_USE_LUA_HPP_I_ SOL_ON + #endif +#elif defined(SOL_USING_CXX_LUA) + #define SOL_USE_LUA_HPP_I_ SOL_OFF +#elif defined(__has_include) + #if __has_include() + #define SOL_USE_LUA_HPP_I_ SOL_ON + #else + #define SOL_USE_LUA_HPP_I_ SOL_OFF + #endif +#else + #define SOL_USE_LUA_HPP_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_CONTAINERS_START) + #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START +#elif defined(SOL_CONTAINERS_START_INDEX) + #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START_INDEX +#elif defined(SOL_CONTAINER_START_INDEX) + #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINER_START_INDEX +#else + #define SOL_CONTAINER_START_INDEX_I_ 1 +#endif + +#if defined (SOL_NO_MEMORY_ALIGNMENT) + #if (SOL_NO_MEMORY_ALIGNMENT != 0) + #define SOL_ALIGN_MEMORY_I_ SOL_OFF + #else + #define SOL_ALIGN_MEMORY_I_ SOL_ON + #endif +#else + #define SOL_ALIGN_MEMORY_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_USE_BOOST) + #if (SOL_USE_BOOST != 0) + #define SOL_USE_BOOST_I_ SOL_ON + #else + #define SOL_USE_BOOST_I_ SOL_OFF + #endif +#else + #define SOL_USE_BOOST_I_ SOL_OFF +#endif + +#if defined(SOL_USE_UNSAFE_BASE_LOOKUP) + #if (SOL_USE_UNSAFE_BASE_LOOKUP != 0) + #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_ON + #else + #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_OFF + #endif +#else + #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_OFF +#endif + +#if defined(SOL_INSIDE_UNREAL) + #if (SOL_INSIDE_UNREAL != 0) + #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_ON + #else + #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_OFF + #endif +#else + #if defined(UE_BUILD_DEBUG) || defined(UE_BUILD_DEVELOPMENT) || defined(UE_BUILD_TEST) || defined(UE_BUILD_SHIPPING) || defined(UE_SERVER) + #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_ON + #else + #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_DEFAULT_OFF + #endif +#endif + +#if defined(SOL_NO_COMPAT) + #if (SOL_NO_COMPAT != 0) + #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_OFF + #else + #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_ON + #endif +#else + #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_DEFAULT_ON +#endif + +#if defined(SOL_GET_FUNCTION_POINTER_UNSAFE) + #if (SOL_GET_FUNCTION_POINTER_UNSAFE != 0) + #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_ON + #else + #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_OFF + #endif +#else + #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_DEFAULT_OFF +#endif + +#if SOL_IS_ON(SOL_COMPILER_FRONTEND_MINGW_I_) && defined(__GNUC__) && (__GNUC__ < 6) + // MinGW is off its rocker in some places... + #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_ON +#else + #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_DEFAULT_OFF +#endif + +// end of sol/version.hpp + +#if SOL_IS_ON(SOL_INSIDE_UNREAL_ENGINE_I_) +#ifdef check +#pragma push_macro("check") +#undef check +#endif +#endif // Unreal Engine 4 Bullshit + +#if SOL_IS_ON(SOL_COMPILER_GCC_I_) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wconversion" +#if __GNUC__ > 6 +#pragma GCC diagnostic ignored "-Wnoexcept-type" +#endif +#elif SOL_IS_ON(SOL_COMPILER_CLANG_I_) +#elif SOL_IS_ON(SOL_COMPILER_VCXX_I_) +#pragma warning(push) +#pragma warning(disable : 4505) // unreferenced local function has been removed GEE THANKS +#endif // clang++ vs. g++ vs. VC++ + +// beginning of sol/forward.hpp + +#ifndef SOL_FORWARD_HPP +#define SOL_FORWARD_HPP + +#include +#include +#include + +#if SOL_IS_ON(SOL_USE_CXX_LUA_I_) || SOL_IS_ON(SOL_USE_CXX_LUAJIT_I_) +struct lua_State; +#else +extern "C" { +struct lua_State; +} +#endif // C++ Mangling for Lua vs. Not + +namespace sol { + + enum class type; + + class stateless_reference; + template + class basic_reference; + using reference = basic_reference; + using main_reference = basic_reference; + class stateless_stack_reference; + class stack_reference; + + template + class basic_bytecode; + + struct lua_value; + + struct proxy_base_tag; + template + struct proxy_base; + template + struct table_proxy; + + template + class basic_table_core; + template + using table_core = basic_table_core; + template + using main_table_core = basic_table_core; + template + using stack_table_core = basic_table_core; + template + using basic_table = basic_table_core; + using table = table_core; + using global_table = table_core; + using main_table = main_table_core; + using main_global_table = main_table_core; + using stack_table = stack_table_core; + using stack_global_table = stack_table_core; + + template + struct basic_lua_table; + using lua_table = basic_lua_table; + using stack_lua_table = basic_lua_table; + + template + class basic_usertype; + template + using usertype = basic_usertype; + template + using stack_usertype = basic_usertype; + + template + class basic_metatable; + using metatable = basic_metatable; + using stack_metatable = basic_metatable; + + template + struct basic_environment; + using environment = basic_environment; + using main_environment = basic_environment; + using stack_environment = basic_environment; + + template + class basic_function; + template + class basic_protected_function; + using unsafe_function = basic_function; + using safe_function = basic_protected_function; + using main_unsafe_function = basic_function; + using main_safe_function = basic_protected_function; + using stack_unsafe_function = basic_function; + using stack_safe_function = basic_protected_function; + using stack_aligned_unsafe_function = basic_function; + using stack_aligned_safe_function = basic_protected_function; + using protected_function = safe_function; + using main_protected_function = main_safe_function; + using stack_protected_function = stack_safe_function; + using stack_aligned_protected_function = stack_aligned_safe_function; +#if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS_I_) + using function = protected_function; + using main_function = main_protected_function; + using stack_function = stack_protected_function; + using stack_aligned_function = stack_aligned_safe_function; +#else + using function = unsafe_function; + using main_function = main_unsafe_function; + using stack_function = stack_unsafe_function; + using stack_aligned_function = stack_aligned_unsafe_function; +#endif + using stack_aligned_stack_handler_function = basic_protected_function; + + struct unsafe_function_result; + struct protected_function_result; + using safe_function_result = protected_function_result; +#if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS_I_) + using function_result = safe_function_result; +#else + using function_result = unsafe_function_result; +#endif + + template + class basic_object_base; + template + class basic_object; + template + class basic_userdata; + template + class basic_lightuserdata; + template + class basic_coroutine; + template + class basic_thread; + + using object = basic_object; + using userdata = basic_userdata; + using lightuserdata = basic_lightuserdata; + using thread = basic_thread; + using coroutine = basic_coroutine; + using main_object = basic_object; + using main_userdata = basic_userdata; + using main_lightuserdata = basic_lightuserdata; + using main_coroutine = basic_coroutine; + using stack_object = basic_object; + using stack_userdata = basic_userdata; + using stack_lightuserdata = basic_lightuserdata; + using stack_thread = basic_thread; + using stack_coroutine = basic_coroutine; + + struct stack_proxy_base; + struct stack_proxy; + struct variadic_args; + struct variadic_results; + struct stack_count; + struct this_state; + struct this_main_state; + struct this_environment; + + class state_view; + class state; + + template + struct as_table_t; + template + struct as_container_t; + template + struct nested; + template + struct light; + template + struct user; + template + struct as_args_t; + template + struct protect_t; + template + struct policy_wrapper; + + template + struct usertype_traits; + template + struct unique_usertype_traits; + + template + struct types { + typedef std::make_index_sequence indices; + static constexpr std::size_t size() { + return sizeof...(Args); + } + }; + + template + struct derive : std::false_type { + typedef types<> type; + }; + + template + struct base : std::false_type { + typedef types<> type; + }; + + template + struct weak_derive { + static bool value; + }; + + template + bool weak_derive::value = false; + + namespace stack { + struct record; + } + +#if SOL_IS_OFF(SOL_USE_BOOST_I_) + template + class optional; + + template + class optional; +#endif + + using check_handler_type = int(lua_State*, int, type, type, const char*); + +} // namespace sol + +#define SOL_BASE_CLASSES(T, ...) \ + namespace sol { \ + template <> \ + struct base : std::true_type { \ + typedef ::sol::types<__VA_ARGS__> type; \ + }; \ + } \ + void a_sol3_detail_function_decl_please_no_collide() +#define SOL_DERIVED_CLASSES(T, ...) \ + namespace sol { \ + template <> \ + struct derive : std::true_type { \ + typedef ::sol::types<__VA_ARGS__> type; \ + }; \ + } \ + void a_sol3_detail_function_decl_please_no_collide() + +#endif // SOL_FORWARD_HPP +// end of sol/forward.hpp + +// beginning of sol/forward_detail.hpp + +#ifndef SOL_FORWARD_DETAIL_HPP +#define SOL_FORWARD_DETAIL_HPP + +// beginning of sol/traits.hpp + +// beginning of sol/tuple.hpp + +// beginning of sol/base_traits.hpp + +#include + +namespace sol { + namespace detail { + struct unchecked_t {}; + const unchecked_t unchecked = unchecked_t{}; + } // namespace detail + + namespace meta { + using sfinae_yes_t = std::true_type; + using sfinae_no_t = std::false_type; + + template + using void_t = void; + + template + using unqualified = std::remove_cv>; + + template + using unqualified_t = typename unqualified::type; + + namespace meta_detail { + template + struct unqualified_non_alias : unqualified {}; + + template