diff --git a/.clang-tidy b/.clang-tidy index 92500ad04d..1f37003f8e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,7 @@ Checks: > -*, portability-*, + -portability-template-virtual-member-function, clang-analyzer-*, -clang-analyzer-optin.*, -clang-analyzer-cplusplus.NewDeleteLeaks, @@ -13,3 +14,7 @@ HeaderFilterRegex: '(apps|components)/' CheckOptions: - key: readability-identifier-naming.ConceptCase value: CamelCase +- key: readability-identifier-naming.NamespaceCase + value: CamelCase +- key: readability-identifier-naming.NamespaceIgnoredRegexp + value: 'bpo|osg(DB|FX|Particle|Shadow|Viewer|Util)?' diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc638729c4..fedc707bd7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,14 +27,14 @@ variables: .Ubuntu_Image: tags: - saas-linux-medium-amd64 - image: ubuntu:22.04 + image: ubuntu:24.04 rules: - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event" Ubuntu_GCC_preprocess: extends: .Ubuntu_Image cache: - key: Ubuntu_GCC_preprocess.ubuntu_22.04.v1 + key: Ubuntu_GCC_preprocess.ubuntu_24.04.v1 paths: - apt-cache/ - .cache/pip/ @@ -43,9 +43,12 @@ Ubuntu_GCC_preprocess: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" before_script: - CI/install_debian_deps.sh openmw-deps openmw-deps-dynamic gcc_preprocess - - pip3 install --user click termtables + - pip3 install --user --break-system-packages click termtables script: - CI/ubuntu_gcc_preprocess.sh + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ID != "7107382" .Ubuntu: extends: .Ubuntu_Image @@ -77,9 +80,9 @@ Ubuntu_GCC_preprocess: - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_detournavigator_navmeshtilescache_benchmark; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_esm_refid_benchmark; fi - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi - - ccache -s + - ccache -svv - df -h - - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi + - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then ~/.local/bin/gcovr --xml-pretty --exclude-unreachable-branches --gcov-ignore-parse-errors=negative_hits.warn_once_per_file --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' - cd .. - df -h @@ -94,12 +97,12 @@ Ubuntu_GCC_preprocess: Coverity: tags: - saas-linux-medium-amd64 - image: ubuntu:22.04 + image: ubuntu:24.04 stage: build rules: - if: $CI_PIPELINE_SOURCE == "schedule" cache: - key: Coverity.ubuntu_22.04.v1 + key: Coverity.ubuntu_24.04.v1 paths: - apt-cache/ - ccache/ @@ -124,7 +127,7 @@ Coverity: - cov-analysis-linux64-*/bin/cov-configure --template --comptype prefix --compiler ccache # 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) - - ccache -s + - ccache -svv after_script: - tar cfz cov-int.tar.gz cov-int - curl https://scan.coverity.com/builds?project=$COVERITY_SCAN_PROJECT_NAME @@ -138,7 +141,7 @@ Coverity: Ubuntu_GCC: extends: .Ubuntu cache: - key: Ubuntu_GCC.ubuntu_22.04.v1 + key: Ubuntu_GCC.ubuntu_24.04.v1 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: @@ -151,7 +154,7 @@ Ubuntu_GCC: Ubuntu_GCC_asan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_asan.ubuntu_24.04.v1 variables: CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -g -O1 -fno-omit-frame-pointer -fsanitize=address -fsanitize=pointer-subtract -fsanitize=leak @@ -164,7 +167,7 @@ Clang_Format: extends: .Ubuntu_Image stage: checks cache: - key: Ubuntu_Clang_Format.ubuntu_22.04.v1 + key: Ubuntu_Clang_Format.ubuntu_24.04.v1 paths: - apt-cache/ variables: @@ -180,11 +183,11 @@ Lupdate: extends: .Ubuntu_Image stage: checks cache: - key: Ubuntu_lupdate.ubuntu_22.04.v1 + key: Ubuntu_lupdate.ubuntu_24.04.v1 paths: - apt-cache/ variables: - LUPDATE: lupdate + LUPDATE: /usr/lib/qt6/bin/lupdate before_script: - CI/install_debian_deps.sh openmw-qt-translations script: @@ -206,7 +209,7 @@ Teal: Ubuntu_GCC_Debug: extends: .Ubuntu cache: - key: Ubuntu_GCC_Debug.ubuntu_22.04.v2 + key: Ubuntu_GCC_Debug.ubuntu_24.04.v2 before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic variables: @@ -222,7 +225,7 @@ Ubuntu_GCC_Debug: Ubuntu_GCC_tests: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -236,7 +239,7 @@ Ubuntu_GCC_tests: .Ubuntu_GCC_tests_Debug: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_Debug.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_Debug.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -252,7 +255,7 @@ Ubuntu_GCC_tests: Ubuntu_GCC_tests_asan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_asan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -268,11 +271,14 @@ Ubuntu_GCC_tests_asan: when: always reports: junit: build/*-tests.xml + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ID != "7107382" Ubuntu_GCC_tests_ubsan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_ubsan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_ubsan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -285,11 +291,14 @@ Ubuntu_GCC_tests_ubsan: when: always reports: junit: build/*-tests.xml + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ID != "7107382" .Ubuntu_GCC_tests_tsan: extends: Ubuntu_GCC cache: - key: Ubuntu_GCC_tests_tsan.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_tsan.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -307,11 +316,15 @@ Ubuntu_GCC_tests_ubsan: Ubuntu_GCC_tests_coverage: extends: .Ubuntu_GCC_tests_Debug cache: - key: Ubuntu_GCC_tests_coverage.ubuntu_22.04.v1 + key: Ubuntu_GCC_tests_coverage.ubuntu_24.04.v1 + paths: + - .cache/pip variables: BUILD_WITH_CODE_COVERAGE: 1 + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" before_script: - CI/install_debian_deps.sh gcc openmw-deps openmw-deps-dynamic openmw-coverage + - pipx install gcovr coverage: /^\s*lines:\s*\d+.\d+\%/ artifacts: paths: [] @@ -322,6 +335,9 @@ Ubuntu_GCC_tests_coverage: coverage_format: cobertura path: coverage.xml junit: build/*-tests.xml + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ID != "7107382" .Ubuntu_Static_Deps: extends: Ubuntu_Clang @@ -333,7 +349,7 @@ Ubuntu_GCC_tests_coverage: - "CI/**/*" - ".gitlab-ci.yml" cache: - key: Ubuntu_Static_Deps.ubuntu_22.04.v1 + key: Ubuntu_Static_Deps.ubuntu_24.04.v1 paths: - apt-cache/ - ccache/ @@ -350,7 +366,7 @@ Ubuntu_GCC_tests_coverage: .Ubuntu_Static_Deps_tests: extends: .Ubuntu_Static_Deps cache: - key: Ubuntu_Static_Deps_tests.ubuntu_22.04.v1 + key: Ubuntu_Static_Deps_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -369,7 +385,7 @@ Ubuntu_Clang: before_script: - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: - key: Ubuntu_Clang.ubuntu_22.04.v2 + key: Ubuntu_Clang.ubuntu_24.04.v2 variables: CC: clang CXX: clang++ @@ -382,7 +398,7 @@ Ubuntu_Clang: before_script: - CI/install_debian_deps.sh clang clang-tidy openmw-deps openmw-deps-dynamic cache: - key: Ubuntu_Clang_Tidy.ubuntu_22.04.v1 + key: Ubuntu_Clang_Tidy.ubuntu_24.04.v1 variables: CMAKE_BUILD_TYPE: Debug CMAKE_CXX_FLAGS_DEBUG: -O0 @@ -396,12 +412,15 @@ Ubuntu_Clang: - cd build - find . -name *.o -exec touch {} \; - cmake --build . -- -j $(nproc) ${BUILD_TARGETS} - - ccache -s + - ccache -svv artifacts: paths: - build/ expire_in: 12h timeout: 3h + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ID != "7107382" Ubuntu_Clang_Tidy_components: extends: .Ubuntu_Clang_Tidy_Base @@ -414,7 +433,7 @@ Ubuntu_Clang_Tidy_openmw: needs: - Ubuntu_Clang_Tidy_components variables: - BUILD_TARGETS: openmw + BUILD_TARGETS: openmw openmw-tests timeout: 3h Ubuntu_Clang_Tidy_openmw-cs: @@ -430,13 +449,13 @@ Ubuntu_Clang_Tidy_other: needs: - Ubuntu_Clang_Tidy_components variables: - BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest components-tests openmw-tests openmw-cs-tests openmw-navmeshtool openmw-bulletobjecttool + BUILD_TARGETS: components-tests bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest openmw-navmeshtool openmw-bulletobjecttool timeout: 3h .Ubuntu_Clang_tests: extends: Ubuntu_Clang cache: - key: Ubuntu_Clang_tests.ubuntu_22.04.v1 + key: Ubuntu_Clang_tests.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -450,7 +469,7 @@ Ubuntu_Clang_Tidy_other: Ubuntu_Clang_tests_Debug: extends: Ubuntu_Clang cache: - key: Ubuntu_Clang_tests_Debug.ubuntu_22.04.v1 + key: Ubuntu_Clang_tests_Debug.ubuntu_24.04.v1 variables: CCACHE_SIZE: 1G BUILD_TESTS_ONLY: 1 @@ -474,7 +493,7 @@ Ubuntu_Clang_tests_Debug: - apt-cache/ before_script: - CI/install_debian_deps.sh $OPENMW_DEPS - - pip3 install --user numpy matplotlib termtables click + - pip3 install --user --break-system-packages numpy matplotlib termtables click script: - CI/run_integration_tests.sh after_script: @@ -485,7 +504,7 @@ Ubuntu_Clang_integration_tests: needs: - Ubuntu_Clang cache: - key: Ubuntu_Clang_integration_tests.ubuntu_22.04.v2 + key: Ubuntu_Clang_integration_tests.ubuntu_24.04.v2 variables: OPENMW_DEPS: openmw-integration-tests @@ -494,15 +513,22 @@ Ubuntu_GCC_integration_tests_asan: needs: - Ubuntu_GCC_asan cache: - key: Ubuntu_GCC_integration_tests_asan.ubuntu_22.04.v1 + key: Ubuntu_GCC_integration_tests_asan.ubuntu_24.04.v1 variables: - OPENMW_DEPS: openmw-integration-tests libasan6 + OPENMW_DEPS: openmw-integration-tests libasan ASAN_OPTIONS: halt_on_error=1:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_leaks=0 .MacOS: stage: build rules: - - if: $CI_PROJECT_ID == "7107382" + - if: $CI_PROJECT_ID != "7107382" + when: never + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: manual + - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "schedule" + image: macos-15-xcode-16 + tags: + - saas-macos-medium-m1 cache: paths: - ccache/ @@ -535,28 +561,28 @@ Ubuntu_GCC_integration_tests_asan: paths: - build/OpenMW-*.dmg -macOS14_Xcode15_amd64: +macOS15_Xcode16_amd64: extends: .MacOS - image: macos-14-xcode-15 - tags: - - saas-macos-medium-m1 cache: - key: macOS14_Xcode15_amd64.v2 + key: macOS15_Xcode16_amd64.v1 variables: CCACHE_SIZE: 3G DMG_IDENTIFIER: amd64 MACOS_AMD64: true + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_EMOJI: true + HOMEBREW_NO_INSTALL_CLEANUP: true -macOS14_Xcode15_arm64: +macOS15_Xcode16_arm64: extends: .MacOS - image: macos-14-xcode-15 - tags: - - saas-macos-medium-m1 cache: - key: macOS14_Xcode15_arm64.v1 + key: macOS15_Xcode16_arm64.v1 variables: DMG_IDENTIFIER: arm64 CCACHE_SIZE: 3G + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_EMOJI: true + HOMEBREW_NO_INSTALL_CLEANUP: true .Compress_And_Upload_Symbols_Base: extends: .Ubuntu_Image @@ -681,7 +707,7 @@ macOS14_Xcode15_arm64: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: ninja-2022-v12 + key: ninja-2022-v13 paths: - ccache - deps @@ -839,7 +865,7 @@ macOS14_Xcode15_arm64: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: - key: msbuild-2022-v12 + key: msbuild-2022-v13 paths: - deps - MSVC2022_64/deps/Qt @@ -940,7 +966,7 @@ Windows_MSBuild_CacheInit: - cd build - cmake --build . -- -j $(nproc) # - cmake --install . # no one uses builds anyway, disable until 'no space left' is resolved - - ccache -s + - ccache -svv - df -h - ls | grep -v -e '^extern$' -e '^install$' | xargs -I '{}' rm -rf './{}' - cd .. @@ -968,7 +994,7 @@ Windows_MSBuild_CacheInit: paths: - .cache/pip before_script: - - pip3 install --user requests click discord_webhook + - pip3 install --user --break-system-packages requests click discord_webhook script: - scripts/find_missing_merge_requests.py --project_id=$CI_PROJECT_ID --ignored_mrs_path=$CI_PROJECT_DIR/.resubmitted_merge_requests.txt diff --git a/AUTHORS.md b/AUTHORS.md index e327bc29d6..c2e71aa85d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -10,7 +10,8 @@ If you feel your name is missing from this list, please add it to `AUTHORS.md`. Programmers ----------- - Bret Curtis (psi29a) - Project leader 2019-present + Alexey Dobrokhotov (Capo) - Project leader 2025-present + Bret Curtis (psi29a) - Project leader 2019-2025 Marc Zinnschlag (Zini) - Project leader 2010-2018 Nicolay Korslund - Project leader 2008-2010 scrawl - Top contributor @@ -49,7 +50,6 @@ Programmers Berulacks Bo Svensson Britt Mathis (galdor557) - Capostrophic Carl Maxwell cc9cii Cédric Mocquillon @@ -197,7 +197,7 @@ Programmers Qlonever Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) - Randy Davin (Kindi) + Randy Davin (Kuyondo) rdimesio rexelion riothamus diff --git a/CHANGELOG.md b/CHANGELOG.md index cbffc23bc7..a2df2e025e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,80 @@ 0.50.0 ------ + Bug #2967: Inventory windows don't update when changing items by script + Bug #4437: Transformations for NiSkinInstance are ignored + Bug #4885: Disable in dialogue result script causes a crash + Bug #5331: Pathfinding works incorrectly when actor is moved from one interior cell to another + Bug #6039: Next Spell keybind fails while selected enchanted item has multiple copies + Bug #6573: Editor: Selection behaves incorrectly on high-DPI displays + Bug #6792: Birth sign info box has no line breaks + Bug #7371: Equipping item from inventory does not play a Down sound when equipping fails + Bug #7622: Player's marksman weapons don't work on close actors underwater + Bug #7649: The sound and vfx of resisted enchanted items' magic still play + Bug #7693: I.ItemUsage should return an item to the selected stack if equipping/consumption is denied + Bug #7740: Magic items in the HUD aren't composited correctly + Bug #7799: Picking up ingredients while object paging active grid is on may cause a hiccup + Bug #7871: Kwama Queen doesn't start combat with player + Bug #7979: Paralyzed NPCs battlecry + Bug #8245: The console command ShowVars does not list global mwscripts + Bug #8265: Topics are linked incorrectly + Bug #8303: On target spells cast by non-actors should fire underwater + Bug #8318: Missing global variables are not handled gracefully in dialogue conditions + Bug #8333: Quest status subrecords should not actually cause parsing to skip remaining data + Bug #8340: Multi-effect enchantments are too expensive + Bug #8341: Repeat shader visitor passes discard parallax + Bug #8349: Travel to non-existent cell causes persistent black screen + Bug #8359: Some quick keys menu related issues + Bug #8371: Silence affects powers + Bug #8375: Moon phase cycle doesn't match Morrowind + Bug #8383: Casting bound helm or boots on beast races doesn't cleanup properly + Bug #8385: Russian encoding broken with locale parameters and calendar + Bug #8404: Prevent merchant equipping breaks on lights + Bug #8408: OpenMW doesn't report all the potential resting hindrances + Bug #8414: Waterwalking works when collision is disabled + Bug #8431: Behaviour of removed items from a container is buggy + Bug #8432: Changing to and from an interior cell doesn't update collision + Bug #8433: Wandering NPCs are not capable of avoiding easy obstacles + Bug #8436: Spell selection in a pinned spellbook window doesn't update + Bug #8437: Pinned inventory window's pin button doesn't look pressed + Bug #8446: Travel prices are strangely inconsistent + Bug #8447: Werewolf swimming animation breaks in third person perspective + Bug #8459: Changing magic effect base cost doesn't change spell price + Bug #8466: Showmap "" reveals nameless cells + Bug #8485: Witchwither disease and probably other common diseases don't work correctly + Bug #8490: Normals on Water disappear when Water Shader is Enabled but Refraction is Disabled + Bug #8500: OpenMW Alarm behaviour doesn't match morrowind.exe + Bug #8519: Multiple bounty is sometimes assigned to player when detected during a pickpocketing action + Bug #8540: Magic resistance is applied to effects without a magnitude + Bug #8557: Charm's disposition changes capped on 100, uncapped below 0 + Bug #8582: addScript-attached local scripts start out inactive + Bug #8585: Dialogue topic list doesn't have enough padding + Bug #8587: Minor INI importer problems + Bug #8593: Render targets do not generate mipmaps + Bug #8598: Post processing shaders don't interact with the vfs correctly + Bug #8599: Non-ASCII paths in BSA files don't work + Bug #8606: Floating point imprecision can mess with container capacity + Bug #8609: The crosshair is too large + Bug #8610: Terrain normal maps using NormalGL format instead of NormalDX + Bug #8612: Using aiactivate on an ingredient when graphical herbalism is enabled triggers non-stop pickup sounds + Bug #8614: Lua garbage collection fails to remove unused data + Bug #8615: Rest/wait time progress speed is different from vanilla + Feature #2522: Support quick item transfer + Feature #3769: Allow GetSpellEffects on enchantments + Feature #6976: [Lua] Weather API + Feature #8077: Save settings changes when clicking "ok"/closing the window + Feature #8112: Expose landscape record data to Lua + Feature #8113: Support extended selection in autodetected subdirectory dialog + Feature #8139: Editor: Redesign the selection markers Feature #8285: Expose list of active shaders in postprocessing API + Feature #8313: Show the character name in the savegame details + Feature #8320: Add access mwscript source text to lua api + Feature #8334: Lua: AddTopic equivalent + Feature #8355: Lua: Window visibility checking in interfaces.UI + Feature #8509: FillJournal script instruction + Feature #8580: Sort characters in the save loading menu + Feature #8597: Lua: Add more built-in event handlers + Feature #8629: Expose path grid data to Lua 0.49.0 ------ diff --git a/CI/before_install.macos.sh b/CI/before_install.macos.sh index f466dd06a7..60d2d3a38c 100755 --- a/CI/before_install.macos.sh +++ b/CI/before_install.macos.sh @@ -1,9 +1,5 @@ #!/bin/sh -ex -export HOMEBREW_NO_EMOJI=1 -export HOMEBREW_NO_INSTALL_CLEANUP=1 -export HOMEBREW_AUTOREMOVE=1 - if [[ "${MACOS_AMD64}" ]]; then ./CI/macos/before_install.amd64.sh else diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 940c72c806..4f62ef03d9 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -377,6 +377,8 @@ case $VS_VERSION in MSVC_DISPLAY_YEAR="2022" QT_MSVC_YEAR="2019" + + VCPKG_TRIPLET="x64-windows" ;; 16|16.0|2019 ) @@ -386,6 +388,8 @@ case $VS_VERSION in MSVC_DISPLAY_YEAR="2019" QT_MSVC_YEAR="2019" + + VCPKG_TRIPLET="x64-windows-2019" ;; 15|15.0|2017 ) @@ -546,7 +550,7 @@ fi QT_VER='6.6.3' AQT_VERSION='v3.1.15' -VCPKG_TAG="2024-11-10" +VCPKG_TAG="2025-07-23" VCPKG_PATH="vcpkg-x64-${VS_VERSION:?}-${VCPKG_TAG:?}" VCPKG_PDB_PATH="vcpkg-x64-${VS_VERSION:?}-pdb-${VCPKG_TAG:?}" VCPKG_MANIFEST="${VCPKG_PATH:?}.txt" @@ -633,16 +637,16 @@ printf "vcpkg packages ${VCPKG_TAG:?}... " fi add_cmake_opts -DCMAKE_TOOLCHAIN_FILE="$(real_pwd)/${VCPKG_PATH:?}/scripts/buildsystems/vcpkg.cmake" - add_cmake_opts -DLuaJit_INCLUDE_DIR="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/include/luajit" - add_cmake_opts -DLuaJit_LIBRARY="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/lib/lua51.lib" + add_cmake_opts -DLuaJit_INCLUDE_DIR="$(real_pwd)/${VCPKG_PATH:?}/installed/${VCPKG_TRIPLET}/include/luajit" + add_cmake_opts -DLuaJit_LIBRARY="$(real_pwd)/${VCPKG_PATH:?}/installed/${VCPKG_TRIPLET}/lib/lua51.lib" for CONFIGURATION in ${CONFIGURATIONS[@]}; do if [[ ${CONFIGURATION:?} == "Debug" ]]; then - VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/debug/bin" + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/${VCPKG_TRIPLET}/debug/bin" add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Debug/MyGUIEngine_d.dll" else - VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/bin" + VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/${VCPKG_TRIPLET}/bin" add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Release/MyGUIEngine.dll" fi @@ -704,17 +708,12 @@ printf "Qt ${QT_VER}... " DLLSUFFIX="" fi - if [ "${QT_MAJOR_VER}" -eq 6 ]; then - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll - # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" - if [ "${QT_MINOR_VER}" -ge 7 ]; then - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" - else - add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" - fi + # Since Qt 6.7.0 plugin is called "qmodernwindowsstyle" + if [ "${QT_MINOR_VER}" -ge 7 ]; then + add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qmodernwindowsstyle${DLLSUFFIX}.dll" else - add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll" fi diff --git a/CI/github.env b/CI/github.env index d8ca8b429f..b793834a8e 100644 --- a/CI/github.env +++ b/CI/github.env @@ -1 +1 @@ -VCPKG_DEPS_TAG=2024-11-10 +VCPKG_DEPS_TAG=2025-07-23 diff --git a/CI/install_debian_deps.sh b/CI/install_debian_deps.sh index 3ba66133ca..4f0e7cdb69 100755 --- a/CI/install_debian_deps.sh +++ b/CI/install_debian_deps.sh @@ -33,10 +33,10 @@ declare -rA GROUPED_DEPS=( libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev + libsdl2-dev libqt6opengl6-dev qt6-tools-dev qt6-tools-dev-tools libopenal-dev libunshield-dev libtinyxml-dev libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev librecast-dev libsqlite3-dev ca-certificates libicu-dev - libyaml-cpp-dev libqt5svg5 libqt5svg5-dev + libyaml-cpp-dev libqt6svg6 libqt6svg6-dev " # These dependencies can alternatively be built and linked statically. @@ -57,22 +57,22 @@ declare -rA GROUPED_DEPS=( libsdl2-dev libboost-system-dev libboost-filesystem-dev libgl-dev " - [openmw-coverage]="gcovr" + [openmw-coverage]="pipx" [openmw-integration-tests]=" ca-certificates gdb git git-lfs - libavcodec58 - libavformat58 - libavutil56 - libboost-iostreams1.74.0 - libboost-program-options1.74.0 - libboost-system1.74.0 + libavcodec60 + libavformat60 + libavutil58 + libboost-iostreams1.83.0 + libboost-program-options1.83.0 + libboost-system1.83.0 libbullet3.24 libcollada-dom2.5-dp0 - libicu70 + libicu74 libjpeg8 libluajit-5.1-2 liblz4-1 @@ -80,19 +80,19 @@ declare -rA GROUPED_DEPS=( libopenal1 libopenscenegraph161 libpng16-16 - libqt5opengl5 + libqt6opengl6 librecast1 libsdl2-2.0-0 libsqlite3-0 - libswresample3 - libswscale5 + libswresample4 + libswscale7 libtinyxml2.6.2v5 libyaml-cpp0.8 python3-pip xvfb " - [libasan6]="libasan6" + [libasan]="libasan8" [android]="binutils build-essential cmake ccache curl unzip git pkg-config" @@ -102,8 +102,8 @@ declare -rA GROUPED_DEPS=( " [openmw-qt-translations]=" - qttools5-dev - qttools5-dev-tools + qt6-tools-dev + qt6-tools-dev-tools git-core " ) diff --git a/CI/macos/before_install.arm64.sh b/CI/macos/before_install.arm64.sh index 84120dfba2..d53d847b1c 100755 --- a/CI/macos/before_install.arm64.sh +++ b/CI/macos/before_install.arm64.sh @@ -3,14 +3,7 @@ brew tap --repair brew update --quiet -brew install curl xquartz gd fontconfig freetype harfbuzz brotli s3cmd - -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@6 - -# Install deps -brew install openal-soft icu4c yaml-cpp sqlite +brew install curl xquartz gd fontconfig freetype harfbuzz brotli s3cmd ccache cmake qt@6 openal-soft icu4c yaml-cpp sqlite curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null diff --git a/CI/macos/ccache_save.sh b/CI/macos/ccache_save.sh index d06d16fb0c..d7cc48c25e 100755 --- a/CI/macos/ccache_save.sh +++ b/CI/macos/ccache_save.sh @@ -1,7 +1,7 @@ #!/bin/sh -ex if [[ "${MACOS_AMD64}" ]]; then - arch -x86_64 ccache -s + arch -x86_64 ccache -svv else - ccache -s + ccache -svv fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 7301496172..57ebeefcfd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,8 +82,8 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 78) -set(OPENMW_POSTPROCESSING_API_REVISION 2) +set(OPENMW_LUA_API_REVISION 87) +set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -249,12 +249,8 @@ endif() find_package(LZ4 REQUIRED) if (USE_QT) - find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5) - if (QT_VERSION_MAJOR VERSION_EQUAL 5) - find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED) - else() - find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) - endif() + find_package(QT REQUIRED COMPONENTS Core NAMES Qt6) + find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED) message(STATUS "Using Qt${QT_VERSION}") endif() @@ -466,7 +462,7 @@ find_package(Boost 1.70.0 CONFIG REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONA if(OPENMW_USE_SYSTEM_MYGUI) find_package(MyGUI 3.4.3 REQUIRED) endif() -find_package(SDL2 2.0.10 REQUIRED) +find_package(SDL2 2.0.20 REQUIRED) find_package(OpenAL REQUIRED) find_package(ZLIB REQUIRED) @@ -590,30 +586,10 @@ if(OPENMW_LTO_BUILD) endif() endif() - -if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) - set(OPENMW_CXX_FLAGS "-Wall -Wextra -Wundef -Wextra-semi -Wno-unused-parameter -pedantic -Wno-long-long -Wnon-virtual-dtor -Wunused ${OPENMW_CXX_FLAGS}") - - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) - # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105438 - set(OPENMW_CXX_FLAGS "-Wno-array-bounds ${OPENMW_CXX_FLAGS}") - endif() - - if (APPLE) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") - endif() - - if (CMAKE_CXX_COMPILER_ID STREQUAL Clang AND NOT APPLE) - if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 3.6) - set(OPENMW_CXX_FLAGS "${OPENMW_CXX_FLAGS} -Wno-potentially-evaluated-expression") - endif () - endif() - - if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) - set(OPENMW_CXX_FLAGS "${OPENMW_CXX_FLAGS} -Wno-unused-but-set-parameter -Wduplicated-branches -Wduplicated-cond -Wlogical-op") - endif() -endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) +if (APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") +endif() # Extern @@ -624,8 +600,42 @@ if (BUILD_OPENCS OR BUILD_OPENCS_TESTS) add_subdirectory (extern/osgQt) endif() +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" OR CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + add_compile_options("/W4") + + set(WARNINGS_DISABLE + 4100 # Unreferenced formal parameter (-Wunused-parameter) + 4127 # Conditional expression is constant + 4996 # Function was declared deprecated + 5054 # Deprecated operations between enumerations of different types caused by Qt headers + ) + + foreach(d ${WARNINGS_DISABLE}) + add_compile_options("/wd${d}") + endforeach(d) + + if(OPENMW_MSVC_WERROR) + add_compile_options("/WX") + endif() +else () + add_compile_options("-Wall" "-Wextra" "-Wundef" "-Wextra-semi" "-Wno-unused-parameter" "-pedantic" "-Wno-long-long" "-Wnon-virtual-dtor" "-Wunused") + + if (CMAKE_CXX_COMPILER_ID STREQUAL Clang AND NOT APPLE) + if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 3.6) + add_compile_options("-Wno-potentially-evaluated-expression") + endif () + endif() + + if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) + add_compile_options("-Wno-unused-but-set-parameter" "-Wduplicated-branches" "-Wduplicated-cond" "-Wlogical-op") + # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105438 + add_compile_options("-Wno-array-bounds") + endif() +endif () + if (OPENMW_CXX_FLAGS) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OPENMW_CXX_FLAGS}") + separate_arguments(OPENMW_CXX_FLAGS NATIVE_COMMAND "${OPENMW_CXX_FLAGS}") + add_compile_options(${OPENMW_CXX_FLAGS}) endif() # Components @@ -715,87 +725,9 @@ if (WIN32) set_target_properties(openmw PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() - # Play a bit with the warning levels - - set(WARNINGS "/W4") - - set(WARNINGS_DISABLE - 4100 # Unreferenced formal parameter (-Wunused-parameter) - 4127 # Conditional expression is constant - 4996 # Function was declared deprecated - 5054 # Deprecated operations between enumerations of different types caused by Qt headers - ) - - foreach(d ${WARNINGS_DISABLE}) - list(APPEND WARNINGS "/wd${d}") - endforeach(d) - - if(OPENMW_MSVC_WERROR) - list(APPEND WARNINGS "/WX") - endif() - - target_compile_options(components PRIVATE ${WARNINGS}) - target_compile_options(osg-ffmpeg-videoplayer PRIVATE ${WARNINGS}) - if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) endif() - - if (BUILD_BSATOOL) - target_compile_options(bsatool PRIVATE ${WARNINGS}) - endif() - - if (BUILD_ESMTOOL) - target_compile_options(esmtool PRIVATE ${WARNINGS}) - endif() - - if (BUILD_ESSIMPORTER) - target_compile_options(openmw-essimporter PRIVATE ${WARNINGS}) - endif() - - if (BUILD_LAUNCHER) - target_compile_options(openmw-launcher PRIVATE ${WARNINGS}) - endif() - - if (BUILD_MWINIIMPORTER) - target_compile_options(openmw-iniimporter PRIVATE ${WARNINGS}) - endif() - - if (BUILD_OPENCS) - target_compile_options(openmw-cs PRIVATE ${WARNINGS}) - endif() - - if (BUILD_OPENMW) - target_compile_options(openmw PRIVATE ${WARNINGS}) - endif() - - if (BUILD_WIZARD) - target_compile_options(openmw-wizard PRIVATE ${WARNINGS}) - endif() - - if (BUILD_COMPONENTS_TESTS) - target_compile_options(components-tests PRIVATE ${WARNINGS}) - endif() - - if (BUILD_BENCHMARKS) - target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ${WARNINGS}) - endif() - - if (BUILD_NAVMESHTOOL) - target_compile_options(openmw-navmeshtool PRIVATE ${WARNINGS}) - endif() - - if (BUILD_BULLETOBJECTTOOL) - target_compile_options(openmw-bulletobjecttool PRIVATE ${WARNINGS} ${MT_BUILD}) - endif() - - if (BUILD_OPENCS_TESTS) - target_compile_options(openmw-cs-tests PRIVATE ${WARNINGS}) - endif() - - if (BUILD_OPENMW_TESTS) - target_compile_options(openmw-tests PRIVATE ${WARNINGS}) - endif() endif(MSVC) # TODO: At some point release builds should not use the console but rather write to a log file diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 5cacedd07e..659d97b7f1 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -155,7 +155,7 @@ namespace VFS::Manager vfs; - VFS::registerArchives(&vfs, fileCollections, archives, true); + VFS::registerArchives(&vfs, fileCollections, archives, true, &encoder.getStatelessEncoder()); Settings::Manager::load(config); diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp index 0a78c33c9e..d2b48ce623 100644 --- a/apps/components_tests/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -139,7 +139,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -147,7 +147,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -156,7 +156,7 @@ namespace ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->removeAgent(mAgentBounds); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -172,7 +172,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -194,7 +194,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; @@ -218,7 +218,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -237,7 +237,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -265,7 +265,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -285,7 +285,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -318,7 +318,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -386,7 +386,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -421,7 +421,7 @@ namespace mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -453,8 +453,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -487,8 +487,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -520,7 +520,7 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -549,7 +549,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -577,7 +577,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -658,7 +658,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -781,7 +781,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -806,7 +806,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::PartialPath); EXPECT_THAT(mPath, @@ -834,7 +834,7 @@ namespace const float endTolerance = 1000.0f; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -979,6 +979,146 @@ namespace EXPECT_EQ(usedNavMeshTiles, 854); } + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 1000, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, -1000, 0, 0, // row 2 + 0, 0, -1000, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125), + Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125), + Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125), + Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 1000, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 12), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(400, 70, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 1000, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 10000), + osg::Vec3f(256, 256, 1000), + osg::Vec3f(-1000, -1000, 0), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam { }; diff --git a/apps/components_tests/esm3/testsaveload.cpp b/apps/components_tests/esm3/testsaveload.cpp index 41a79313cc..7e5b73e4dd 100644 --- a/apps/components_tests/esm3/testsaveload.cpp +++ b/apps/components_tests/esm3/testsaveload.cpp @@ -723,7 +723,7 @@ namespace ESM TEST_P(Esm3SaveLoadRecordTest, landShouldNotChange) { LandRecordData data; - std::iota(data.mHeights.begin(), data.mHeights.end(), 1); + std::iota(data.mHeights.begin(), data.mHeights.end(), 1.0f); std::for_each(data.mHeights.begin(), data.mHeights.end(), [](float& v) { v *= Land::sHeightScale; }); data.mMinHeight = *std::min_element(data.mHeights.begin(), data.mHeights.end()); data.mMaxHeight = *std::max_element(data.mHeights.begin(), data.mHeights.end()); diff --git a/apps/components_tests/fx/lexer.cpp b/apps/components_tests/fx/lexer.cpp index 9c095a1f64..976f88d7a3 100644 --- a/apps/components_tests/fx/lexer.cpp +++ b/apps/components_tests/fx/lexer.cpp @@ -5,7 +5,7 @@ namespace { using namespace testing; - using namespace fx::Lexer; + using namespace Fx::Lexer; struct LexerTest : Test { diff --git a/apps/components_tests/fx/technique.cpp b/apps/components_tests/fx/technique.cpp index ad57074b18..b04ff5c52a 100644 --- a/apps/components_tests/fx/technique.cpp +++ b/apps/components_tests/fx/technique.cpp @@ -91,7 +91,7 @@ namespace )" }; using namespace testing; - using namespace fx; + using namespace Fx; struct TechniqueTest : Test { @@ -113,7 +113,8 @@ namespace void compile(const std::string& name) { - mTechnique = std::make_unique(*mVFS.get(), mImageManager, name, 1, 1, true, true); + mTechnique = std::make_unique( + *mVFS.get(), mImageManager, Technique::makeFileName(name), name, 1, 1, true, true); mTechnique->compile(); } }; diff --git a/apps/components_tests/lua/testl10n.cpp b/apps/components_tests/lua/testl10n.cpp index b48028a730..7446822f7f 100644 --- a/apps/components_tests/lua/testl10n.cpp +++ b/apps/components_tests/lua/testl10n.cpp @@ -24,6 +24,8 @@ namespace constexpr VFS::Path::NormalizedView test2EnPath("l10n/test2/en.yaml"); constexpr VFS::Path::NormalizedView test3EnPath("l10n/test3/en.yaml"); constexpr VFS::Path::NormalizedView test3DePath("l10n/test3/de.yaml"); + constexpr VFS::Path::NormalizedView test4RuPath("l10n/test4/ru.yaml"); + constexpr VFS::Path::NormalizedView test4EnPath("l10n/test4/en.yaml"); VFSTestFile invalidScript("not a script"); VFSTestFile incorrectScript( @@ -69,6 +71,16 @@ currency: "You have {money, number, currency}" VFSTestFile test2En(R"X( good_morning: "Morning!" you_have_arrows: "Arrows count: {count}" +)X"); + + VFSTestFile test4Ru(R"X( +skill_increase: "Ваш навык {навык} увеличился до {value}" +acrobatics: "Акробатика" +)X"); + + VFSTestFile test4En(R"X( +stat_increase: "Your {stat} has increased to {value}" +speed: "Speed" )X"); struct LuaL10nTest : Test @@ -80,6 +92,8 @@ you_have_arrows: "Arrows count: {count}" { test2EnPath, &test2En }, { test3EnPath, &test1En }, { test3DePath, &test1De }, + { test4RuPath, &test4Ru }, + { test4EnPath, &test4En }, }); LuaUtil::ScriptsConfiguration mCfg; @@ -91,7 +105,7 @@ you_have_arrows: "Arrows count: {count}" lua.protectedCall([&](LuaUtil::LuaView& view) { sol::state_view& l = view.sol(); internal::CaptureStdout(); - l10n::Manager l10nManager(mVFS.get()); + L10n::Manager l10nManager(mVFS.get()); l10nManager.setPreferredLocales({ "de", "en" }); EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: gmst de en\n"); @@ -169,6 +183,18 @@ you_have_arrows: "Arrows count: {count}" l.safe_script("t3 = l10n('Test3', 'de')"); l10nManager.setPreferredLocales({ "en" }); EXPECT_EQ(get(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!"); + + // Test that formatting arguments use a correct encoding + l.safe_script("t4 = l10n('Test4', 'ru')"); + l10nManager.setPreferredLocales({ "ru", "en" }); + EXPECT_EQ(get(l, "t4('skill_increase', {навык='Акробатика', value=100})"), + "Ваш навык Акробатика увеличился до 100"); + EXPECT_EQ(get(l, "t4('skill_increase', {навык=t4('acrobatics'), value=100})"), + "Ваш навык Акробатика увеличился до 100"); + EXPECT_EQ(get(l, "t4('stat_increase', {stat='Speed', value=100})"), + "Your Speed has increased to 100"); + EXPECT_EQ(get(l, "t4('stat_increase', {stat=t4('speed'), value=100})"), + "Your Speed has increased to 100"); }); } } diff --git a/apps/components_tests/lua/testscriptscontainer.cpp b/apps/components_tests/lua/testscriptscontainer.cpp index 4f3cca1b87..9c4c656b32 100644 --- a/apps/components_tests/lua/testscriptscontainer.cpp +++ b/apps/components_tests/lua/testscriptscontainer.cpp @@ -638,8 +638,9 @@ CUSTOM: customdata.lua sol::object deserialized = LuaUtil::deserialize(lua.sol(), data2.mScripts[0].mData, &serializer1); EXPECT_TRUE(deserialized.is()); sol::table table = deserialized; - for (const auto& [key, value] : table) + if (!table.empty()) { + const auto [key, value] = *table.cbegin(); EXPECT_TRUE(key.is()); EXPECT_TRUE(value.is()); EXPECT_EQ(key.as(), (ESM::RefNum{ 42, 34 })); diff --git a/apps/components_tests/lua/testutilpackage.cpp b/apps/components_tests/lua/testutilpackage.cpp index a61c0e0306..47041985f6 100644 --- a/apps/components_tests/lua/testutilpackage.cpp +++ b/apps/components_tests/lua/testutilpackage.cpp @@ -9,22 +9,33 @@ namespace { using namespace testing; + struct LuaUtilPackageTest : Test + { + LuaUtil::LuaState mLuaState{ nullptr, nullptr }; + + LuaUtilPackageTest() + { + mLuaState.addInternalLibSearchPath( + std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "components" / "lua"); + sol::state_view sol = mLuaState.unsafeState(); + sol["util"] = LuaUtil::initUtilPackage(sol); + } + }; + template - T get(sol::state& lua, const std::string& luaCode) + T get(sol::state_view& lua, const std::string& luaCode) { return lua.safe_script("return " + luaCode).get(); } - std::string getAsString(sol::state& lua, std::string luaCode) + std::string getAsString(sol::state_view& lua, std::string luaCode) { return LuaUtil::toString(lua.safe_script("return " + luaCode)); } - TEST(LuaUtilPackageTest, Vector2) + TEST_F(LuaUtilPackageTest, Vector2) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua.safe_script("v = util.vector2(3, 4)"); EXPECT_FLOAT_EQ(get(lua, "v.x"), 3); EXPECT_FLOAT_EQ(get(lua, "v.y"), 4); @@ -55,11 +66,9 @@ namespace EXPECT_TRUE(get(lua, "swizzle['01'] == util.vector2(0, 1) and swizzle['0y'] == util.vector2(0, 2)")); } - TEST(LuaUtilPackageTest, Vector3) + TEST_F(LuaUtilPackageTest, Vector3) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua.safe_script("v = util.vector3(5, 12, 13)"); EXPECT_FLOAT_EQ(get(lua, "v.x"), 5); EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); @@ -94,11 +103,9 @@ namespace get(lua, "swizzle['001'] == util.vector3(0, 0, 1) and swizzle['0yx'] == util.vector3(0, 2, 1)")); } - TEST(LuaUtilPackageTest, Vector4) + TEST_F(LuaUtilPackageTest, Vector4) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua.safe_script("v = util.vector4(5, 12, 13, 15)"); EXPECT_FLOAT_EQ(get(lua, "v.x"), 5); EXPECT_FLOAT_EQ(get(lua, "v.y"), 12); @@ -136,11 +143,9 @@ namespace lua, "swizzle['0001'] == util.vector4(0, 0, 0, 1) and swizzle['0yx1'] == util.vector4(0, 2, 1, 1)")); } - TEST(LuaUtilPackageTest, Color) + TEST_F(LuaUtilPackageTest, Color) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua.safe_script("brown = util.color.rgba(0.75, 0.25, 0, 1)"); EXPECT_EQ(get(lua, "tostring(brown)"), "(0.75, 0.25, 0, 1)"); lua.safe_script("blue = util.color.rgb(0, 1, 0, 1)"); @@ -155,11 +160,9 @@ namespace EXPECT_TRUE(get(lua, "red:asRgb() == util.vector3(1, 0, 0)")); } - TEST(LuaUtilPackageTest, Transform) + TEST_F(LuaUtilPackageTest, Transform) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua["T"] = lua["util"]["transform"]; lua["v"] = lua["util"]["vector3"]; EXPECT_ERROR(lua.safe_script("T.identity = nil"), "attempt to index"); @@ -191,11 +194,9 @@ namespace EXPECT_LT(get(lua, "(rz_move_rx:inverse() * v(0, 1, 2) - v(1, 2, 3)):length()"), 1e-6); } - TEST(LuaUtilPackageTest, UtilityFunctions) + TEST_F(LuaUtilPackageTest, UtilityFunctions) { - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::string); - lua["util"] = LuaUtil::initUtilPackage(lua); + sol::state_view lua = mLuaState.unsafeState(); lua.safe_script("v = util.vector2(1, 0):rotate(math.rad(120))"); EXPECT_FLOAT_EQ(get(lua, "v.x"), -0.5f); EXPECT_FLOAT_EQ(get(lua, "v.y"), 0.86602539f); @@ -203,6 +204,10 @@ namespace EXPECT_FLOAT_EQ(get(lua, "util.clamp(0.1, 0, 1.5)"), 0.1f); EXPECT_FLOAT_EQ(get(lua, "util.clamp(-0.1, 0, 1.5)"), 0); EXPECT_FLOAT_EQ(get(lua, "util.clamp(2.1, 0, 1.5)"), 1.5f); + EXPECT_FLOAT_EQ(get(lua, "util.round(2.1)"), 2.0f); + EXPECT_FLOAT_EQ(get(lua, "util.round(-2.1)"), -2.0f); + EXPECT_FLOAT_EQ(get(lua, "util.remap(5, 0, 10, 0, 100)"), 50.0f); + EXPECT_FLOAT_EQ(get(lua, "util.remap(-5, 0, 10, 0, 100)"), -50.0f); lua.safe_script("t = util.makeReadOnly({x = 1})"); EXPECT_FLOAT_EQ(get(lua, "t.x"), 1); EXPECT_ERROR(lua.safe_script("t.y = 2"), "userdata value"); diff --git a/apps/components_tests/misc/testresourcehelpers.cpp b/apps/components_tests/misc/testresourcehelpers.cpp index 05079ae875..b21ecb2e14 100644 --- a/apps/components_tests/misc/testresourcehelpers.cpp +++ b/apps/components_tests/misc/testresourcehelpers.cpp @@ -26,6 +26,15 @@ namespace std::unique_ptr mVFS = TestingOpenMW::createTestVFS({}); constexpr VFS::Path::NormalizedView path("sound/foo.wav"); EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); + + auto correctESM4SoundPath = [](auto path, auto* vfs) { + return Misc::ResourceHelpers::correctResourcePath({ { "sound" } }, path, vfs, ".mp3"); + }; + + EXPECT_EQ(correctESM4SoundPath("foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("SOUND/foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("DATA\\SOUND\\foo.WAV", mVFS.get()), "sound\\foo.mp3"); + EXPECT_EQ(correctESM4SoundPath("\\Data/Sound\\foo.WAV", mVFS.get()), "sound\\foo.mp3"); } namespace diff --git a/apps/esmtool/esmtool.cpp b/apps/esmtool/esmtool.cpp index 0473676f93..7cdc2bcd98 100644 --- a/apps/esmtool/esmtool.cpp +++ b/apps/esmtool/esmtool.cpp @@ -215,8 +215,6 @@ int main(int argc, char** argv) std::cerr << "ERROR: " << e.what() << std::endl; return 1; } - - return 0; } namespace diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp index 095e36c7ee..520dd27f2a 100644 --- a/apps/essimporter/converter.hpp +++ b/apps/essimporter/converter.hpp @@ -249,7 +249,7 @@ namespace ESSImport { ESM::InventoryState& invState = mContext->mPlayer.mObject.mInventory; - for (size_t i = 0; i < invState.mItems.size(); ++i) + for (uint32_t i = 0; i < static_cast(invState.mItems.size()); ++i) { // FIXME: in case of conflict (multiple items with this refID) use the already equipped one? if (invState.mItems[i].mRef.mRefID == refr.mActorData.mSelectedEnchantItem) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 735bcf1df1..38b0446efc 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -242,11 +242,36 @@ void Launcher::GraphicsPage::handleWindowModeChange(Settings::WindowMode mode) { if (mode == Settings::WindowMode::Fullscreen || mode == Settings::WindowMode::WindowedFullscreen) { + QString customSizeMessage = tr("Custom window size is available only in Windowed mode."); + QString windowBorderMessage = tr("Window border is available only in Windowed mode."); + standardRadioButton->toggle(); customRadioButton->setEnabled(false); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); windowBorderCheckBox->setEnabled(false); + windowBorderCheckBox->setToolTip(windowBorderMessage); + customWidthSpinBox->setToolTip(customSizeMessage); + customHeightSpinBox->setToolTip(customSizeMessage); + customRadioButton->setToolTip(customSizeMessage); + } + + if (mode == Settings::WindowMode::Fullscreen) + { + resolutionComboBox->setEnabled(true); + resolutionComboBox->setToolTip(""); + standardRadioButton->setToolTip(""); + } + else if (mode == Settings::WindowMode::WindowedFullscreen) + { + QString fullScreenMessage = tr("Windowed Fullscreen mode always uses the native display resolution."); + + resolutionComboBox->setEnabled(false); + resolutionComboBox->setToolTip(fullScreenMessage); + standardRadioButton->setToolTip(fullScreenMessage); + + // Assume that a first item is a native screen resolution + resolutionComboBox->setCurrentIndex(0); } else { @@ -254,6 +279,13 @@ void Launcher::GraphicsPage::handleWindowModeChange(Settings::WindowMode mode) customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); windowBorderCheckBox->setEnabled(true); + resolutionComboBox->setEnabled(true); + resolutionComboBox->setToolTip(""); + standardRadioButton->setToolTip(""); + windowBorderCheckBox->setToolTip(""); + customWidthSpinBox->setToolTip(""); + customHeightSpinBox->setToolTip(""); + customRadioButton->setToolTip(""); } } diff --git a/apps/launcher/importpage.cpp b/apps/launcher/importpage.cpp index 3ad6e538da..461ba4c2eb 100644 --- a/apps/launcher/importpage.cpp +++ b/apps/launcher/importpage.cpp @@ -88,11 +88,7 @@ void Launcher::ImportPage::on_importerButton_clicked() // Create the file if it doesn't already exist, else the importer will fail auto path = mCfgMgr.getUserConfigPath(); path /= "openmw.cfg"; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QFile file(path); -#else - QFile file(Files::pathToQString(path)); -#endif if (!file.exists()) { diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 2ea152305f..ff85154289 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -42,7 +42,7 @@ int runLauncher(int argc, char* argv[]) resourcesPath = Files::pathToQString(variables["resources"].as().u8string()); } - l10n::installQtTranslations(app, "launcher", resourcesPath); + L10n::installQtTranslations(app, "launcher", resourcesPath); Launcher::MainDialog mainWin(configurationManager); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 07face085f..2aa80d346c 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -497,11 +497,7 @@ bool Launcher::MainDialog::writeSettings() } // Game settings -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QFile file(userPath / Files::openmwCfgFile); -#else - QFile file(Files::getUserConfigPathQString(mCfgMgr)); -#endif if (!file.open(QIODevice::ReadWrite | QIODevice::Text)) { diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index dfddc45bc5..27fa2ca27c 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -163,8 +163,6 @@ bool Launcher::SettingsPage::loadSettings() loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox); loadSettingBool( Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox); - loadSettingBool( - Settings::game().mUnarmedCreatureAttacksDamageArmor, *unarmedCreatureAttacksDamageArmorCheckBox); loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox); } @@ -373,8 +371,6 @@ void Launcher::SettingsPage::saveSettings() saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads); saveSettingBool( *allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface); - saveSettingBool( - *unarmedCreatureAttacksDamageArmorCheckBox, Settings::game().mUnarmedCreatureAttacksDamageArmor); saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType); } diff --git a/apps/launcher/ui/settingspage.ui b/apps/launcher/ui/settingspage.ui index e792ac2843..b96d734605 100644 --- a/apps/launcher/ui/settingspage.ui +++ b/apps/launcher/ui/settingspage.ui @@ -53,7 +53,7 @@ - + <html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html> @@ -63,7 +63,7 @@ - + <html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html> @@ -73,7 +73,7 @@ - + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> @@ -123,7 +123,7 @@ - + <html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html> @@ -133,7 +133,7 @@ - + <html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html> @@ -143,7 +143,7 @@ - + <html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html> @@ -153,7 +153,7 @@ - + <html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html> @@ -183,7 +183,7 @@ - + <html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html> @@ -203,7 +203,7 @@ - + <html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html> @@ -213,16 +213,6 @@ - - - - <html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html> - - - Unarmed Creature Attacks Damage Armor - - - diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index a8dee709da..4b8e7acd61 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -9,8 +9,6 @@ #include #include -namespace sfs = std::filesystem; - namespace { // from configfileparser.cpp diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index 6e4242cb4e..c5f21ac67f 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -10,7 +10,6 @@ #include namespace bpo = boost::program_options; -namespace sfs = std::filesystem; #ifndef _WIN32 int main(int argc, char* argv[]) diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index 08ed10c3b3..6cfa7fc61d 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -189,7 +189,7 @@ namespace NavMeshTool VFS::Manager vfs; - VFS::registerArchives(&vfs, fileCollections, archives, true); + VFS::registerArchives(&vfs, fileCollections, archives, true, &encoder.getStatelessEncoder()); Settings::Manager::load(config); diff --git a/apps/navmeshtool/worldspacedata.cpp b/apps/navmeshtool/worldspacedata.cpp index ec44d33e6c..df5c48e7ab 100644 --- a/apps/navmeshtool/worldspacedata.cpp +++ b/apps/navmeshtool/worldspacedata.cpp @@ -201,8 +201,8 @@ namespace NavMeshTool { if (!land.has_value() || osg::Vec2i(land->mX, land->mY) != cellPosition || (land->mDataTypes & ESM::Land::DATA_VHGT) == 0) - return { HeightfieldPlane{ ESM::Land::DEFAULT_HEIGHT }, ESM::Land::DEFAULT_HEIGHT, - ESM::Land::DEFAULT_HEIGHT }; + return { HeightfieldPlane{ static_cast(ESM::Land::DEFAULT_HEIGHT) }, + static_cast(ESM::Land::DEFAULT_HEIGHT), static_cast(ESM::Land::DEFAULT_HEIGHT) }; ESM::Land::LandData& landData = *landDatas.emplace_back(std::make_unique()); land->loadData(ESM::Land::DATA_VHGT, landData); diff --git a/apps/niftest/niftest.cpp b/apps/niftest/niftest.cpp index 8634134665..7f19008bd1 100644 --- a/apps/niftest/niftest.cpp +++ b/apps/niftest/niftest.cpp @@ -113,7 +113,7 @@ bool isBSA(const std::filesystem::path& path) std::unique_ptr makeArchive(const std::filesystem::path& path) { if (isBSA(path)) - return VFS::makeBsaArchive(path); + return VFS::makeBsaArchive(path, nullptr); if (std::filesystem::is_directory(path)) return std::make_unique(path); return nullptr; @@ -198,7 +198,7 @@ void readVFS(std::unique_ptr&& archive, const std::filesystem::pat { try { - readVFS(VFS::makeBsaArchive(file.second), file.second, quiet); + readVFS(VFS::makeBsaArchive(file.second, nullptr), file.second, quiet); } catch (const std::exception& e) { diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index a131c56358..c9ca7838be 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -88,7 +88,7 @@ opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller - cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands + cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands objectmarker ) opencs_units (view/render @@ -240,11 +240,7 @@ target_link_libraries(openmw-cs-lib components_qt ) -if (QT_VERSION_MAJOR VERSION_EQUAL 6) - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) -else() - target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg) -endif() +target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) if (WIN32) target_sources(openmw-cs PRIVATE ${CMAKE_SOURCE_DIR}/files/windows/openmw-cs.exe.manifest) diff --git a/apps/opencs/model/filter/textnode.cpp b/apps/opencs/model/filter/textnode.cpp index 7d837f9e54..e8b89c5277 100644 --- a/apps/opencs/model/filter/textnode.cpp +++ b/apps/opencs/model/filter/textnode.cpp @@ -34,11 +34,11 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons QString string; - if (data.type() == QVariant::String) + if (data.typeId() == QMetaType::QString) { string = data.toString(); } - else if ((data.type() == QVariant::Int || data.type() == QVariant::UInt) + else if ((data.typeId() == QMetaType::Int || data.typeId() == QMetaType::UInt) && CSMWorld::Columns::hasEnums(static_cast(mColumnId))) { int value = data.toInt(); @@ -49,7 +49,7 @@ bool CSMFilter::TextNode::test(const CSMWorld::IdTableBase& table, int row, cons if (value >= 0 && value < static_cast(enums.size())) string = QString::fromUtf8(enums[value].second.c_str()); } - else if (data.type() == QVariant::Bool) + else if (data.typeId() == QMetaType::Bool) { string = data.toBool() ? "true" : "false"; } diff --git a/apps/opencs/model/filter/valuenode.cpp b/apps/opencs/model/filter/valuenode.cpp index 264967f240..abbf00aecd 100644 --- a/apps/opencs/model/filter/valuenode.cpp +++ b/apps/opencs/model/filter/valuenode.cpp @@ -29,8 +29,8 @@ bool CSMFilter::ValueNode::test(const CSMWorld::IdTableBase& table, int row, con QVariant data = table.data(index); - if (data.type() != QVariant::Double && data.type() != QVariant::Bool && data.type() != QVariant::Int - && data.type() != QVariant::UInt && data.type() != static_cast(QMetaType::Float)) + if (data.typeId() != QMetaType::Double && data.typeId() != QMetaType::Bool && data.typeId() != QMetaType::Int + && data.typeId() != QMetaType::UInt && data.typeId() != QMetaType::Float) return false; double value = data.toDouble(); diff --git a/apps/opencs/model/prefs/shortcuteventhandler.cpp b/apps/opencs/model/prefs/shortcuteventhandler.cpp index 4bbda2996f..a7e9dfbb3a 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.cpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.cpp @@ -62,39 +62,31 @@ namespace CSMPrefs { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int)keyEvent->modifiers(); - unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) - return activate(widget, mod, key); + return activate(widget, keyEvent->keyCombination()); } else if (event->type() == QEvent::KeyRelease) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); - unsigned int mod = (unsigned int)keyEvent->modifiers(); - unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) - return deactivate(widget, mod, key); + return deactivate(widget, keyEvent->keyCombination()); } else if (event->type() == QEvent::MouseButtonPress) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int)mouseEvent->modifiers(); - unsigned int button = (unsigned int)mouseEvent->button(); - return activate(widget, mod, button); + return activate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button()))); } else if (event->type() == QEvent::MouseButtonRelease) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); - unsigned int mod = (unsigned int)mouseEvent->modifiers(); - unsigned int button = (unsigned int)mouseEvent->button(); - return deactivate(widget, mod, button); + return deactivate(widget, QKeyCombination(mouseEvent->modifiers(), Qt::Key(mouseEvent->button()))); } else if (event->type() == QEvent::FocusOut) { @@ -149,7 +141,7 @@ namespace CSMPrefs } } - bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) + bool ShortcutEventHandler::activate(QWidget* widget, QKeyCombination keyCombination) { std::vector> potentials; bool used = false; @@ -167,7 +159,7 @@ namespace CSMPrefs if (!shortcut->isEnabled()) continue; - if (checkModifier(mod, button, shortcut, true)) + if (checkModifier(keyCombination, shortcut, true)) used = true; if (shortcut->getActivationStatus() != Shortcut::AS_Inactive) @@ -175,7 +167,8 @@ namespace CSMPrefs int pos = shortcut->getPosition(); int lastPos = shortcut->getLastPosition(); - MatchResult result = match(mod, button, shortcut->getSequence()[pos]); + MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(), + shortcut->getSequence()[pos].toCombined()); if (result == Matches_WithMod || result == Matches_NoMod) { @@ -220,10 +213,8 @@ namespace CSMPrefs return used; } - bool ShortcutEventHandler::deactivate(QWidget* widget, unsigned int mod, unsigned int button) + bool ShortcutEventHandler::deactivate(QWidget* widget, QKeyCombination keyCombination) { - const int KeyMask = 0x01FFFFFF; - bool used = false; while (widget) @@ -235,11 +226,11 @@ namespace CSMPrefs { Shortcut* shortcut = *it; - if (checkModifier(mod, button, shortcut, false)) + if (checkModifier(keyCombination, shortcut, false)) used = true; int pos = shortcut->getPosition(); - MatchResult result = match(0, button, shortcut->getSequence()[pos] & KeyMask); + MatchResult result = match(0, keyCombination.key(), shortcut->getSequence()[pos].key()); if (result != Matches_Not) { @@ -268,13 +259,13 @@ namespace CSMPrefs return used; } - bool ShortcutEventHandler::checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate) + bool ShortcutEventHandler::checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate) { if (!shortcut->isEnabled() || !shortcut->getModifier() || shortcut->getSecondaryMode() == Shortcut::SM_Ignore || shortcut->getModifierStatus() == activate) return false; - MatchResult result = match(mod, button, shortcut->getModifier()); + MatchResult result = match(keyCombination.keyboardModifiers(), keyCombination.key(), shortcut->getModifier()); bool used = false; if (result != Matches_Not) diff --git a/apps/opencs/model/prefs/shortcuteventhandler.hpp b/apps/opencs/model/prefs/shortcuteventhandler.hpp index 2093e259e9..cc94797450 100644 --- a/apps/opencs/model/prefs/shortcuteventhandler.hpp +++ b/apps/opencs/model/prefs/shortcuteventhandler.hpp @@ -42,11 +42,11 @@ namespace CSMPrefs void updateParent(QWidget* widget); - bool activate(QWidget* widget, unsigned int mod, unsigned int button); + bool activate(QWidget* widget, QKeyCombination keyCombination); - bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); + bool deactivate(QWidget* widget, QKeyCombination keyCombination); - bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); + bool checkModifier(QKeyCombination keyCombination, Shortcut* shortcut, bool activate); MatchResult match(unsigned int mod, unsigned int button, unsigned int value); diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index ac032efffb..2ffa042641 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -115,15 +115,12 @@ namespace CSMPrefs std::string ShortcutManager::convertToString(const QKeySequence& sequence) const { - const int MouseKeyMask = 0x01FFFFFF; - const int ModMask = 0x7E000000; - std::string result; - for (int i = 0; i < (int)sequence.count(); ++i) + for (int i = 0; i < sequence.count(); ++i) { - int mods = sequence[i] & ModMask; - int key = sequence[i] & MouseKeyMask; + int mods = sequence[i].keyboardModifiers(); + int key = sequence[i].key(); if (key) { diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 5c32ddb68b..4d191b90a4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -59,13 +59,6 @@ void CSMPrefs::State::declare() .setTooltip("Minimum width of subviews.") .setRange(50, 10000); declareEnum(mValues->mWindows.mMainwindowScrollbar, "Main Window Horizontal Scrollbar Mode"); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") - .setTooltip( - "When \"Grow then Scroll\" option is selected, the window size grows to" - " the width of the virtual desktop. \nIf this option is selected the the window growth" - "is limited to the current screen."); -#endif declareCategory("Records"); declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format"); @@ -180,7 +173,10 @@ void CSMPrefs::State::declare() declareInt(mValues->mRendering.mCameraOrthoSize, "Orthographic Projection Size Parameter") .setTooltip("Size of the orthographic frustum, greater value will allow the camera to see more of the world.") .setRange(10, 10000); - declareDouble(mValues->mRendering.mObjectMarkerAlpha, "Object Marker Transparency").setPrecision(2).setRange(0, 1); + declareDouble(mValues->mRendering.mObjectMarkerScale, "Object Marker Scale Factor") + .setPrecision(2) + .setRange(.01f, 100.f) + .setTooltip("Multiplier for the size of object selection markers."); declareBool(mValues->mRendering.mSceneUseGradient, "Use Gradient Background"); declareColour(mValues->mRendering.mSceneDayBackgroundColour, "Day Background Colour"); declareColour(mValues->mRendering.mSceneDayGradientColour, "Day Gradient Colour") @@ -376,6 +372,7 @@ void CSMPrefs::State::declare() declareShortcut(mValues->mKeyBindings.mSceneScaleSubmode, "Scale Object Submode"); declareShortcut(mValues->mKeyBindings.mSceneRotateSubmode, "Rotate Object Submode"); declareShortcut(mValues->mKeyBindings.mSceneCameraCycle, "Cycle Camera Mode"); + declareShortcut(mValues->mKeyBindings.mSceneToggleMarker, "Toggle Selection Marker"); declareSubcategory("1st/Free Camera"); declareShortcut(mValues->mKeyBindings.mFreeForward, "Forward"); diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp index 1339fa62ed..7b6d5e9f5f 100644 --- a/apps/opencs/model/prefs/values.hpp +++ b/apps/opencs/model/prefs/values.hpp @@ -258,7 +258,7 @@ namespace CSMPrefs Settings::SettingValue mCameraFov{ mIndex, sName, "camera-fov", 90 }; Settings::SettingValue mCameraOrtho{ mIndex, sName, "camera-ortho", false }; Settings::SettingValue mCameraOrthoSize{ mIndex, sName, "camera-ortho-size", 100 }; - Settings::SettingValue mObjectMarkerAlpha{ mIndex, sName, "object-marker-alpha", 0.5 }; + Settings::SettingValue mObjectMarkerScale{ mIndex, sName, "object-marker-scale", 5.0 }; Settings::SettingValue mSceneUseGradient{ mIndex, sName, "scene-use-gradient", true }; Settings::SettingValue mSceneDayBackgroundColour{ mIndex, sName, "scene-day-background-colour", "#6e7880" }; @@ -491,7 +491,7 @@ namespace CSMPrefs Settings::SettingValue mSceneScaleSubmode{ mIndex, sName, "scene-submode-scale", "V" }; Settings::SettingValue mSceneRotateSubmode{ mIndex, sName, "scene-submode-rotate", "R" }; Settings::SettingValue mSceneCameraCycle{ mIndex, sName, "scene-cam-cycle", "Tab" }; - Settings::SettingValue mSceneToggleMarkers{ mIndex, sName, "scene-toggle-markers", "F4" }; + Settings::SettingValue mSceneToggleMarker{ mIndex, sName, "scene-toggle-marker", "F4" }; Settings::SettingValue mFreeForward{ mIndex, sName, "free-forward", "W" }; Settings::SettingValue mFreeBackward{ mIndex, sName, "free-backward", "S" }; Settings::SettingValue mFreeLeft{ mIndex, sName, "free-left", "A" }; @@ -507,8 +507,10 @@ namespace CSMPrefs Settings::SettingValue mOrbitRollRight{ mIndex, sName, "orbit-roll-right", "E" }; Settings::SettingValue mOrbitSpeedMode{ mIndex, sName, "orbit-speed-mode", "" }; Settings::SettingValue mOrbitCenterSelection{ mIndex, sName, "orbit-center-selection", "C" }; - Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", "" }; - Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", "" }; + Settings::SettingValue mScriptEditorComment{ mIndex, sName, "script-editor-comment", + "Ctrl+Slash" }; + Settings::SettingValue mScriptEditorUncomment{ mIndex, sName, "script-editor-uncomment", + "Ctrl+Shift+Question" }; }; struct ModelsCategory : Settings::WithIndex diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index 00e5fec7b0..fa9251b949 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -143,7 +143,7 @@ CSMWorld::Data::Data(ToUTF8::FromType encoding, const Files::PathContainer& data , mArchives(archives) , mVFS(std::make_unique()) { - VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); + VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true, &mEncoder.getStatelessEncoder()); mResourcesManager.setVFS(mVFS.get()); @@ -1465,7 +1465,7 @@ std::vector CSMWorld::Data::getIds(bool listDeleted) const void CSMWorld::Data::assetsChanged() { mVFS.get()->reset(); - VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); + VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true, &mEncoder.getStatelessEncoder()); const UniversalId assetTableIds[] = { UniversalId::Type_Meshes, UniversalId::Type_Icons, UniversalId::Type_Musics, UniversalId::Type_SoundsRes, UniversalId::Type_Textures, UniversalId::Type_Videos }; diff --git a/apps/opencs/model/world/infoselectwrapper.cpp b/apps/opencs/model/world/infoselectwrapper.cpp index 2e9ed6e150..1ce126e0ee 100644 --- a/apps/opencs/model/world/infoselectwrapper.cpp +++ b/apps/opencs/model/world/infoselectwrapper.cpp @@ -625,8 +625,6 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue( default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } - - return false; } template @@ -651,8 +649,6 @@ bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue( default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } - - return false; } QVariant CSMWorld::ConstInfoSelectWrapper::getValue() const diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index e0d5799726..6fff14674f 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -753,7 +753,6 @@ void CSMWorld::RefIdCollection::cloneRecord( bool CSMWorld::RefIdCollection::touchRecord(const ESM::RefId& id) { throw std::runtime_error("RefIdCollection::touchRecord is unimplemented"); - return false; } void CSMWorld::RefIdCollection::appendRecord(std::unique_ptr record, UniversalId::Type type) diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index afcab50ead..ffc7400da3 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -659,11 +659,7 @@ void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string // mScrollbarOnly = windows["mainwindow-scrollbar"].toString() == "Scrollbar Only"; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - updateWidth(windows["grow-limit"].isTrue(), minWidth); -#else updateWidth(true, minWidth); -#endif mSubViewWindow.addDockWidget(Qt::TopDockWidgetArea, view); diff --git a/apps/opencs/view/filter/editwidget.hpp b/apps/opencs/view/filter/editwidget.hpp index f2ecd5f642..7efbd8e7a3 100644 --- a/apps/opencs/view/filter/editwidget.hpp +++ b/apps/opencs/view/filter/editwidget.hpp @@ -50,7 +50,7 @@ namespace CSVFilter std::pair operator()(const QVariant& variantData) { FilterType filterType = FilterType::String; - QMetaType::Type dataType = static_cast(variantData.type()); + QMetaType::Type dataType = static_cast(variantData.typeId()); if (dataType == QMetaType::QString || dataType == QMetaType::Bool || dataType == QMetaType::Int) filterType = FilterType::String; if (dataType == QMetaType::Int || dataType == QMetaType::Float) diff --git a/apps/opencs/view/render/cell.cpp b/apps/opencs/view/render/cell.cpp index 3d3b82acf8..8399ea52a0 100644 --- a/apps/opencs/view/render/cell.cpp +++ b/apps/opencs/view/render/cell.cpp @@ -25,9 +25,10 @@ #include "cellwater.hpp" #include "instancedragmodes.hpp" #include "mask.hpp" -#include "object.hpp" +#include "objectmarker.hpp" #include "pathgrid.hpp" #include "terrainstorage.hpp" +#include "worldspacewidget.hpp" #include #include @@ -107,9 +108,6 @@ bool CSVRender::Cell::addObjects(int start, int end) auto object = std::make_unique(mData, mCellNode, id, false); - if (mSubModeElementMask & Mask_Reference) - object->setSubMode(mSubMode); - mObjects.insert(std::make_pair(id, object.release())); modified = true; } @@ -168,9 +166,10 @@ void CSVRender::Cell::unloadLand() mCellBorder.reset(); } -CSVRender::Cell::Cell( - CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted, bool isExterior) - : mData(document.getData()) +CSVRender::Cell::Cell(CSMDoc::Document& document, ObjectMarker* selectionMarker, osg::Group* rootNode, + const std::string& id, bool deleted, bool isExterior) + : mSelectionMarker(selectionMarker) + , mData(document.getData()) , mId(ESM::RefId::stringRefId(id)) , mDeleted(deleted) , mSubMode(0) @@ -466,7 +465,10 @@ void CSVRender::Cell::setSelection(int elementMask, Selection mode) } iter->second->setSelected(selected); + if (selected) + mSelectionMarker->addToSelectionHistory(iter->second->getReferenceId(), false); } + mSelectionMarker->updateSelectionMarker(); } if (mPathgrid && elementMask & Mask_Pathgrid) { @@ -506,8 +508,10 @@ void CSVRender::Cell::selectAllWithSameParentId(int elementMask) if (!iter->second->getSelected() && ids.find(iter->second->getReferenceableId()) != ids.end()) { iter->second->setSelected(true); + mSelectionMarker->addToSelectionHistory(iter->second->getReferenceId(), false); } } + mSelectionMarker->updateSelectionMarker(); } void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) @@ -520,6 +524,9 @@ void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) else if (dragMode == DragMode_Select_Invert) object->setSelected(!object->getSelected()); + + if (object->getSelected()) + mSelectionMarker->addToSelectionHistory(object->getReferenceId(), false); } void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) @@ -542,6 +549,8 @@ void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3 } } } + + mSelectionMarker->updateSelectionMarker(); } void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) @@ -555,6 +564,8 @@ void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distan if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode); } + + mSelectionMarker->updateSelectionMarker(); } void CSVRender::Cell::setCellArrows(int mask) @@ -625,9 +636,11 @@ void CSVRender::Cell::selectFromGroup(const std::vector& group) if (objectName == object->getReferenceId()) { object->setSelected(true, osg::Vec4f(1, 0, 1, 1)); + mSelectionMarker->addToSelectionHistory(object->getReferenceId(), false); } } } + mSelectionMarker->updateSelectionMarker(); } void CSVRender::Cell::unhideAll() @@ -673,8 +686,7 @@ void CSVRender::Cell::setSubMode(int subMode, unsigned int elementMask) mSubModeElementMask = elementMask; if (elementMask & Mask_Reference) - for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) - iter->second->setSubMode(subMode); + mSelectionMarker->setSubMode(subMode); } void CSVRender::Cell::reset(unsigned int elementMask) @@ -685,3 +697,11 @@ void CSVRender::Cell::reset(unsigned int elementMask) if (mPathgrid && elementMask & Mask_Pathgrid) mPathgrid->resetIndicators(); } + +CSVRender::Object* CSVRender::Cell::getObjectByReferenceId(const std::string& referenceId) +{ + if (auto iter = mObjects.find(Misc::StringUtils::lowerCase(referenceId)); iter != mObjects.end()) + return iter->second; + else + return nullptr; +} diff --git a/apps/opencs/view/render/cell.hpp b/apps/opencs/view/render/cell.hpp index 5ec8d87c33..093a047d65 100644 --- a/apps/opencs/view/render/cell.hpp +++ b/apps/opencs/view/render/cell.hpp @@ -9,9 +9,9 @@ #include #include -#include "../../model/doc/document.hpp" #include "../../model/world/cellcoordinates.hpp" #include "instancedragmodes.hpp" +#include "worldspacewidget.hpp" #include #include @@ -44,8 +44,11 @@ namespace CSVRender class CellBorder; class CellMarker; + class ObjectMarker; + class Cell { + ObjectMarker* const mSelectionMarker; CSMWorld::Data& mData; ESM::RefId mId; osg::ref_ptr mCellNode; @@ -90,8 +93,8 @@ namespace CSVRender public: /// \note Deleted covers both cells that are deleted and cells that don't exist in /// the first place. - Cell(CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted = false, - bool isExterior = false); + Cell(CSMDoc::Document& document, ObjectMarker* selectionMarker, osg::Group* rootNode, const std::string& id, + bool deleted = false, bool isExterior = false); ~Cell(); @@ -182,6 +185,8 @@ namespace CSVRender /// true state. void reset(unsigned int elementMask); + CSVRender::Object* getObjectByReferenceId(const std::string& referenceId); + friend class CellNodeCallback; }; } diff --git a/apps/opencs/view/render/instancemode.cpp b/apps/opencs/view/render/instancemode.cpp index 03872a3d6c..e100a69a7c 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -362,7 +362,29 @@ CSVRender::InstanceMode::InstanceMode( for (const char axis : "xyz") connect(new CSMPrefs::Shortcut(std::string("scene-axis-") + axis, worldspaceWidget), - qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, axis] { this->setDragAxis(axis); }); + qOverload<>(&CSMPrefs::Shortcut::activated), this, [this, axis] { + this->setDragAxis(axis); + std::string axisStr(1, toupper(axis)); + switch (getSubMode()) + { + case (Object::Mode_Move): + axisStr += "_Axis"; + break; + case (Object::Mode_Rotate): + axisStr += "_Axis_Rot"; + break; + case (Object::Mode_Scale): + axisStr += "_Axis_Scale"; + break; + } + + auto selectionMarker = getWorldspaceWidget().getSelectionMarker(); + + if (mDragAxis != -1) + selectionMarker->updateMarkerHighlight(axisStr, axis - 'x'); + else + selectionMarker->resetMarkerHighlight(); + }); } void CSVRender::InstanceMode::activate(CSVWidget::SceneToolbar* toolbar) @@ -460,52 +482,58 @@ void CSVRender::InstanceMode::secondaryEditPressed(const WorldspaceHitResult& hi void CSVRender::InstanceMode::primarySelectPressed(const WorldspaceHitResult& hit) { - getWorldspaceWidget().clearSelection(Mask_Reference); + auto& worldspaceWidget = getWorldspaceWidget(); - if (hit.tag) + worldspaceWidget.clearSelection(Mask_Reference); + + if (!hit.tag) + return; + + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { - if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) - { - // hit an Object, select it - CSVRender::Object* object = objectTag->mObject; - object->setSelected(true); - return; - } + // hit an Object, select it + CSVRender::Object* object = objectTag->mObject; + object->setSelected(true); + worldspaceWidget.getSelectionMarker()->addToSelectionHistory(object->getReferenceId()); } } void CSVRender::InstanceMode::secondarySelectPressed(const WorldspaceHitResult& hit) { - if (hit.tag) + if (!hit.tag) + return; + + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { - if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) - { - // hit an Object, toggle its selection state - CSVRender::Object* object = objectTag->mObject; - object->setSelected(!object->getSelected()); - return; - } + // hit an Object, toggle its selection state + CSVRender::Object* object = objectTag->mObject; + object->setSelected(!object->getSelected()); + + const auto selectionMarker = getWorldspaceWidget().getSelectionMarker(); + + if (object->getSelected()) + selectionMarker->addToSelectionHistory(object->getReferenceId(), false); + + selectionMarker->updateSelectionMarker(); } } void CSVRender::InstanceMode::tertiarySelectPressed(const WorldspaceHitResult& hit) { - auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); - - if (snapTarget) + if (auto* snapTarget + = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get())) { snapTarget->mObject->setSnapTarget(false); } - if (hit.tag) + if (!hit.tag) + return; + + if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { - if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) - { - // hit an Object, toggle its selection state - CSVRender::Object* object = objectTag->mObject; - object->setSnapTarget(!object->getSnapTarget()); - return; - } + // hit an Object, toggle its selection state + CSVRender::Object* object = objectTag->mObject; + object->setSnapTarget(!object->getSnapTarget()); } } @@ -514,23 +542,26 @@ bool CSVRender::InstanceMode::primaryEditStartDrag(const QPoint& pos) if (mDragMode != DragMode_None || mLocked) return false; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + auto& worldspaceWidget = getWorldspaceWidget(); - std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + WorldspaceHitResult hit = worldspaceWidget.mousePick(pos, worldspaceWidget.getInteractionMask()); + + std::vector> selection = worldspaceWidget.getSelection(Mask_Reference); if (selection.empty()) { // Only change selection at the start of drag if no object is already selected if (hit.tag && CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { - getWorldspaceWidget().clearSelection(Mask_Reference); + worldspaceWidget.clearSelection(Mask_Reference); if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { CSVRender::Object* object = objectTag->mObject; object->setSelected(true); + worldspaceWidget.getSelectionMarker()->addToSelectionHistory(object->getReferenceId()); } } - selection = getWorldspaceWidget().getSelection(Mask_Reference); + selection = worldspaceWidget.getSelection(Mask_Reference); if (selection.empty()) return false; } @@ -591,23 +622,26 @@ bool CSVRender::InstanceMode::secondaryEditStartDrag(const QPoint& pos) if (mDragMode != DragMode_None || mLocked) return false; - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); + auto& worldspaceWidget = getWorldspaceWidget(); - std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); + WorldspaceHitResult hit = worldspaceWidget.mousePick(pos, worldspaceWidget.getInteractionMask()); + + std::vector> selection = worldspaceWidget.getSelection(Mask_Reference); if (selection.empty()) { // Only change selection at the start of drag if no object is already selected if (hit.tag && CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { - getWorldspaceWidget().clearSelection(Mask_Reference); + worldspaceWidget.clearSelection(Mask_Reference); if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { CSVRender::Object* object = objectTag->mObject; object->setSelected(true); + worldspaceWidget.getSelectionMarker()->addToSelectionHistory(object->getReferenceId()); } } - selection = getWorldspaceWidget().getSelection(Mask_Reference); + selection = worldspaceWidget.getSelection(Mask_Reference); if (selection.empty()) return false; } @@ -641,10 +675,10 @@ bool CSVRender::InstanceMode::secondaryEditStartDrag(const QPoint& pos) mDragMode = DragMode_Scale_Snap; // Calculate scale factor - std::vector> editedSelection = getWorldspaceWidget().getEdited(Mask_Reference); + std::vector> editedSelection = worldspaceWidget.getEdited(Mask_Reference); osg::Vec3f center = getScreenCoords(getSelectionCenter(editedSelection)); - int widgetHeight = getWorldspaceWidget().height(); + int widgetHeight = worldspaceWidget.height(); float dx = pos.x() - center.x(); float dy = (widgetHeight - pos.y()) - center.y(); @@ -1098,7 +1132,7 @@ void CSVRender::InstanceMode::dropEvent(QDropEvent* event) return; WorldspaceHitResult hit - = getWorldspaceWidget().mousePick(event->pos(), getWorldspaceWidget().getInteractionMask()); + = getWorldspaceWidget().mousePick(event->position().toPoint(), getWorldspaceWidget().getInteractionMask()); std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index fe4b6e9b7f..30eac77eb9 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -18,25 +18,11 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include #include -#include -#include -#include #include -#include "../../model/prefs/state.hpp" #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/commands.hpp" @@ -63,11 +49,6 @@ namespace ESM struct Light; } -const float CSVRender::Object::MarkerShaftWidth = 30; -const float CSVRender::Object::MarkerShaftBaseLength = 70; -const float CSVRender::Object::MarkerHeadWidth = 50; -const float CSVRender::Object::MarkerHeadLength = 50; - namespace { @@ -95,12 +76,6 @@ QString CSVRender::ObjectTag::getToolTip(bool /*hideBasics*/, const WorldspaceHi return QString::fromUtf8(mObject->getReferenceableId().c_str()); } -CSVRender::ObjectMarkerTag::ObjectMarkerTag(Object* object, int axis) - : ObjectTag(object) - , mAxis(axis) -{ -} - void CSVRender::Object::clear() {} void CSVRender::Object::update() @@ -204,238 +179,6 @@ const CSMWorld::CellRef& CSVRender::Object::getReference() const return mData.getReferences().getRecord(mReferenceId).get(); } -void CSVRender::Object::updateMarker() -{ - for (int i = 0; i < 3; ++i) - { - if (mMarker[i]) - { - mRootNode->removeChild(mMarker[i]); - mMarker[i] = osg::ref_ptr(); - } - - if (mSelected) - { - if (mSubMode == 0) - { - mMarker[i] = makeMoveOrScaleMarker(i); - mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - - mRootNode->addChild(mMarker[i]); - } - else if (mSubMode == 1) - { - mMarker[i] = makeRotateMarker(i); - mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - - mRootNode->addChild(mMarker[i]); - } - else if (mSubMode == 2) - { - mMarker[i] = makeMoveOrScaleMarker(i); - mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); - - mRootNode->addChild(mMarker[i]); - } - } - } -} - -osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker(int axis) -{ - osg::ref_ptr geometry(new osg::Geometry); - - float shaftLength = MarkerShaftBaseLength + mBaseNode->getBound().radius(); - - // shaft - osg::Vec3Array* vertices = new osg::Vec3Array; - - for (int i = 0; i < 2; ++i) - { - float length = i ? shaftLength : MarkerShaftWidth; - - vertices->push_back(getMarkerPosition(-MarkerShaftWidth / 2, -MarkerShaftWidth / 2, length, axis)); - vertices->push_back(getMarkerPosition(-MarkerShaftWidth / 2, MarkerShaftWidth / 2, length, axis)); - vertices->push_back(getMarkerPosition(MarkerShaftWidth / 2, MarkerShaftWidth / 2, length, axis)); - vertices->push_back(getMarkerPosition(MarkerShaftWidth / 2, -MarkerShaftWidth / 2, length, axis)); - } - - // head backside - vertices->push_back(getMarkerPosition(-MarkerHeadWidth / 2, -MarkerHeadWidth / 2, shaftLength, axis)); - vertices->push_back(getMarkerPosition(-MarkerHeadWidth / 2, MarkerHeadWidth / 2, shaftLength, axis)); - vertices->push_back(getMarkerPosition(MarkerHeadWidth / 2, MarkerHeadWidth / 2, shaftLength, axis)); - vertices->push_back(getMarkerPosition(MarkerHeadWidth / 2, -MarkerHeadWidth / 2, shaftLength, axis)); - - // head - vertices->push_back(getMarkerPosition(0, 0, shaftLength + MarkerHeadLength, axis)); - - geometry->setVertexArray(vertices); - - osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); - - // shaft - for (int i = 0; i < 4; ++i) - { - int i2 = i == 3 ? 0 : i + 1; - primitives->push_back(i); - primitives->push_back(4 + i); - primitives->push_back(i2); - - primitives->push_back(4 + i); - primitives->push_back(4 + i2); - primitives->push_back(i2); - } - - // cap - primitives->push_back(0); - primitives->push_back(1); - primitives->push_back(2); - - primitives->push_back(2); - primitives->push_back(3); - primitives->push_back(0); - - // head, backside - primitives->push_back(0 + 8); - primitives->push_back(1 + 8); - primitives->push_back(2 + 8); - - primitives->push_back(2 + 8); - primitives->push_back(3 + 8); - primitives->push_back(0 + 8); - - for (int i = 0; i < 4; ++i) - { - primitives->push_back(12); - primitives->push_back(8 + (i == 3 ? 0 : i + 1)); - primitives->push_back(8 + i); - } - - geometry->addPrimitiveSet(primitives); - - osg::Vec4Array* colours = new osg::Vec4Array; - - for (int i = 0; i < 8; ++i) - colours->push_back( - osg::Vec4f(axis == 0 ? 1.0f : 0.2f, axis == 1 ? 1.0f : 0.2f, axis == 2 ? 1.0f : 0.2f, mMarkerTransparency)); - - for (int i = 8; i < 8 + 4 + 1; ++i) - colours->push_back( - osg::Vec4f(axis == 0 ? 1.0f : 0.0f, axis == 1 ? 1.0f : 0.0f, axis == 2 ? 1.0f : 0.0f, mMarkerTransparency)); - - geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); - - setupCommonMarkerState(geometry); - - osg::ref_ptr group(new osg::Group); - group->addChild(geometry); - - return group; -} - -osg::ref_ptr CSVRender::Object::makeRotateMarker(int axis) -{ - const float InnerRadius = std::max(MarkerShaftBaseLength, mBaseNode->getBound().radius()); - const float OuterRadius = InnerRadius + MarkerShaftWidth; - - const float SegmentDistance = 100.f; - const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); - const size_t VerticesPerSegment = 4; - const size_t IndicesPerSegment = 24; - - const size_t VertexCount = SegmentCount * VerticesPerSegment; - const size_t IndexCount = SegmentCount * IndicesPerSegment; - - const float Angle = 2 * osg::PI / SegmentCount; - - const unsigned short IndexPattern[IndicesPerSegment] - = { 0, 4, 5, 0, 5, 1, 2, 6, 4, 2, 4, 0, 3, 7, 6, 3, 6, 2, 1, 5, 7, 1, 7, 3 }; - - osg::ref_ptr geometry = new osg::Geometry(); - - osg::ref_ptr vertices = new osg::Vec3Array(VertexCount); - osg::ref_ptr colors = new osg::Vec4Array(1); - osg::ref_ptr primitives - = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, IndexCount); - - // prevent some depth collision issues from overlaps - osg::Vec3f offset = getMarkerPosition(0, MarkerShaftWidth / 4, 0, axis); - - for (size_t i = 0; i < SegmentCount; ++i) - { - size_t index = i * VerticesPerSegment; - - float innerX = InnerRadius * std::cos(i * Angle); - float innerY = InnerRadius * std::sin(i * Angle); - - float outerX = OuterRadius * std::cos(i * Angle); - float outerY = OuterRadius * std::sin(i * Angle); - - vertices->at(index++) = getMarkerPosition(innerX, innerY, MarkerShaftWidth / 2, axis) + offset; - vertices->at(index++) = getMarkerPosition(innerX, innerY, -MarkerShaftWidth / 2, axis) + offset; - vertices->at(index++) = getMarkerPosition(outerX, outerY, MarkerShaftWidth / 2, axis) + offset; - vertices->at(index++) = getMarkerPosition(outerX, outerY, -MarkerShaftWidth / 2, axis) + offset; - } - - colors->at(0) - = osg::Vec4f(axis == 0 ? 1.0f : 0.2f, axis == 1 ? 1.0f : 0.2f, axis == 2 ? 1.0f : 0.2f, mMarkerTransparency); - - for (size_t i = 0; i < SegmentCount; ++i) - { - size_t indices[IndicesPerSegment]; - for (size_t j = 0; j < IndicesPerSegment; ++j) - { - indices[j] = i * VerticesPerSegment + j; - - if (indices[j] >= VertexCount) - indices[j] -= VertexCount; - } - - size_t elementOffset = i * IndicesPerSegment; - for (size_t j = 0; j < IndicesPerSegment; ++j) - { - primitives->setElement(elementOffset++, indices[IndexPattern[j]]); - } - } - - geometry->setVertexArray(vertices); - geometry->setColorArray(colors, osg::Array::BIND_OVERALL); - geometry->addPrimitiveSet(primitives); - - setupCommonMarkerState(geometry); - - osg::ref_ptr group = new osg::Group(); - group->addChild(geometry); - - return group; -} - -void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geometry) -{ - osg::ref_ptr state = geometry->getOrCreateStateSet(); - state->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - state->setMode(GL_BLEND, osg::StateAttribute::ON); - - state->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); -} - -osg::Vec3f CSVRender::Object::getMarkerPosition(float x, float y, float z, int axis) -{ - switch (axis) - { - case 2: - return osg::Vec3f(x, y, z); - case 0: - return osg::Vec3f(z, x, y); - case 1: - return osg::Vec3f(y, z, x); - - default: - - throw std::logic_error("invalid axis for marker geometry"); - } -} - CSVRender::Object::Object( CSMWorld::Data& data, osg::Group* parentNode, const std::string& id, bool referenceable, bool forceBaseToZero) : mData(data) @@ -446,8 +189,6 @@ CSVRender::Object::Object( , mForceBaseToZero(forceBaseToZero) , mScaleOverride(1) , mOverrideFlags(0) - , mSubMode(-1) - , mMarkerTransparency(0.5f) { mRootNode = new osg::PositionAttitudeTransform; @@ -476,7 +217,6 @@ CSVRender::Object::Object( adjustTransform(); update(); - updateMarker(); } CSVRender::Object::~Object() @@ -506,9 +246,6 @@ void CSVRender::Object::setSelected(bool selected, const osg::Vec4f& color) } else mRootNode->addChild(mBaseNode); - - mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble(); - updateMarker(); } bool CSVRender::Object::getSelected() const @@ -536,9 +273,6 @@ void CSVRender::Object::setSnapTarget(bool isSnapTarget) } else mRootNode->addChild(mBaseNode); - - mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble(); - updateMarker(); } bool CSVRender::Object::getSnapTarget() const @@ -566,7 +300,6 @@ bool CSVRender::Object::referenceableDataChanged(const QModelIndex& topLeft, con { adjustTransform(); update(); - updateMarker(); return true; } @@ -614,7 +347,6 @@ bool CSVRender::Object::referenceDataChanged(const QModelIndex& topLeft, const Q = ESM::RefId::stringRefId(references.getData(index, columnIndex).toString().toUtf8().constData()); update(); - updateMarker(); } return true; @@ -626,7 +358,6 @@ bool CSVRender::Object::referenceDataChanged(const QModelIndex& topLeft, const Q void CSVRender::Object::reloadAssets() { update(); - updateMarker(); } std::string CSVRender::Object::getReferenceId() const @@ -720,12 +451,6 @@ void CSVRender::Object::setScale(float scale) adjustTransform(); } -void CSVRender::Object::setMarkerTransparency(float value) -{ - mMarkerTransparency = value; - updateMarker(); -} - void CSVRender::Object::apply(CSMWorld::CommandMacro& commands) { const CSMWorld::RefCollection& collection = mData.getReferences(); @@ -796,18 +521,8 @@ void CSVRender::Object::apply(CSMWorld::CommandMacro& commands) mOverrideFlags = 0; } -void CSVRender::Object::setSubMode(int subMode) -{ - if (subMode != mSubMode) - { - mSubMode = subMode; - updateMarker(); - } -} - void CSVRender::Object::reset() { mOverrideFlags = 0; adjustTransform(); - updateMarker(); } diff --git a/apps/opencs/view/render/object.hpp b/apps/opencs/view/render/object.hpp index 31f0d93ac4..fc36776c25 100644 --- a/apps/opencs/view/render/object.hpp +++ b/apps/opencs/view/render/object.hpp @@ -58,14 +58,6 @@ namespace CSVRender QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; }; - class ObjectMarkerTag : public ObjectTag - { - public: - ObjectMarkerTag(Object* object, int axis); - - int mAxis; - }; - class Object { public: @@ -76,12 +68,22 @@ namespace CSVRender Override_Scale = 4 }; - private: - static const float MarkerShaftWidth; - static const float MarkerShaftBaseLength; - static const float MarkerHeadWidth; - static const float MarkerHeadLength; + enum SubMode + { + Mode_Move, + Mode_Rotate, + Mode_Scale, + Mode_None, + }; + enum Axis + { + Axis_X, + Axis_Y, + Axis_Z + }; + + private: CSMWorld::Data& mData; ESM::RefId mReferenceId; ESM::RefId mReferenceableId; @@ -96,9 +98,6 @@ namespace CSVRender ESM::Position mPositionOverride; float mScaleOverride; int mOverrideFlags; - osg::ref_ptr mMarker[3]; - int mSubMode; - float mMarkerTransparency; std::unique_ptr mActor; /// Not implemented @@ -120,16 +119,6 @@ namespace CSVRender /// Throws an exception if *this was constructed with referenceable const CSMWorld::CellRef& getReference() const; - void updateMarker(); - - osg::ref_ptr makeMoveOrScaleMarker(int axis); - osg::ref_ptr makeRotateMarker(int axis); - - /// Sets up a stateset with properties common to all marker types. - void setupCommonMarkerState(osg::ref_ptr geometry); - - osg::Vec3f getMarkerPosition(float x, float y, float z, int axis); - public: Object(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, bool referenceable, bool forceBaseToZero = false); @@ -199,8 +188,6 @@ namespace CSVRender /// Apply override changes via command and end edit mode void apply(CSMWorld::CommandMacro& commands); - void setSubMode(int subMode); - /// Erase all overrides and restore the visual representation of the object to its /// true state. void reset(); diff --git a/apps/opencs/view/render/objectmarker.cpp b/apps/opencs/view/render/objectmarker.cpp new file mode 100644 index 0000000000..e21436430f --- /dev/null +++ b/apps/opencs/view/render/objectmarker.cpp @@ -0,0 +1,307 @@ +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "../../model/prefs/state.hpp" +#include "objectmarker.hpp" +#include "worldspacewidget.hpp" + +namespace +{ + class FindMaterialVisitor : public osg::NodeVisitor + { + public: + FindMaterialVisitor(CSVRender::NodeMap& map) + : osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN) + , mMap(map) + { + } + + void apply(osg::Geometry& node) override + { + osg::StateSet* state = node.getStateSet(); + if (state->getAttribute(osg::StateAttribute::MATERIAL)) + mMap.emplace(node.getName(), &node); + + traverse(node); + } + + private: + CSVRender::NodeMap& mMap; + }; + + class ToCamera : public SceneUtil::NodeCallback + { + public: + ToCamera(osg::ref_ptr clipPlane) + : mClipPlane(std::move(clipPlane)) + { + } + void operator()(osg::Node* node, osgUtil::CullVisitor* cv) + { + osg::Vec3f normal = cv->getEyePoint(); + mClipPlane->setClipPlane(normal.x(), normal.y(), normal.z(), 0); + traverse(node, cv); + } + + private: + osg::ref_ptr mClipPlane; + }; + + auto addTagToActiveMarkerNodes = [](CSVRender::NodeMap& mMarkerNodes, CSVRender::Object* object, + std::initializer_list suffixes) { + for (const auto& markerSuffix : suffixes) + { + for (char axis = 'X'; axis <= 'Z'; ++axis) + mMarkerNodes[axis + markerSuffix]->setUserData(new CSVRender::ObjectMarkerTag(object, axis - 'X')); + } + }; +} + +namespace CSVRender +{ + ObjectMarkerTag::ObjectMarkerTag(Object* object, int axis) + : ObjectTag(object) + , mAxis(axis) + { + } + + ObjectMarker::ObjectMarker(WorldspaceWidget* worldspaceWidget, Resource::ResourceSystem* resourceSystem) + : mWorldspaceWidget(worldspaceWidget) + , mResourceSystem(resourceSystem) + , mMarkerScale(CSMPrefs::get()["Rendering"]["object-marker-scale"].toDouble()) + , mSubMode(Object::Mode_None) + { + mBaseNode = new osg::PositionAttitudeTransform; + mBaseNode->setNodeMask(Mask_Reference); + mBaseNode->setScale(osg::Vec3f(mMarkerScale, mMarkerScale, mMarkerScale)); + + mRootNode = new osg::PositionAttitudeTransform; + mRootNode->addChild(mBaseNode); + worldspaceWidget->setSelectionMarkerRoot(mRootNode); + + QFile file(":render/selection-marker"); + + if (!file.open(QIODevice::ReadOnly)) + throw std::runtime_error("Failed to open selection marker file"); + + auto markerData = file.readAll(); + + mResourceSystem->getSceneManager()->loadSelectionMarker(mBaseNode, markerData.data(), markerData.size()); + + osg::ref_ptr baseNodeState = mBaseNode->getOrCreateStateSet(); + baseNodeState->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + baseNodeState->setRenderBinDetails(1000, "RenderBin"); + + FindMaterialVisitor matMapper(mMarkerNodes); + + mBaseNode->accept(matMapper); + + for (const auto& [name, node] : mMarkerNodes) + { + osg::StateSet* state = node->getStateSet(); + osg::Material* mat = static_cast(state->getAttribute(osg::StateAttribute::MATERIAL)); + osg::Vec4f emis = mat->getEmission(osg::Material::FRONT_AND_BACK); + mat->setEmission(osg::Material::FRONT_AND_BACK, emis / 4); + mOriginalColors.emplace(name, emis); + } + + SceneUtil::NodeMap sceneNodes; + SceneUtil::NodeMapVisitor nodeMapper(sceneNodes); + mBaseNode->accept(nodeMapper); + + mMarkerNodes.insert(sceneNodes.begin(), sceneNodes.end()); + + osg::ref_ptr rotateMarkers = mMarkerNodes["rotateMarkers"]; + osg::ClipPlane* clip = new osg::ClipPlane(0); + rotateMarkers->setCullCallback(new ToCamera(clip)); + rotateMarkers->getStateSet()->setAttributeAndModes(clip, osg::StateAttribute::ON); + } + + void ObjectMarker::toggleVisibility() + { + bool isVisible = mBaseNode->getNodeMask() == Mask_Reference; + mBaseNode->setNodeMask(isVisible ? Mask_Hidden : Mask_Reference); + } + + void ObjectMarker::updateScale(const float scale) + { + mMarkerScale = scale; + mBaseNode->setScale(osg::Vec3f(scale, scale, scale)); + } + + void ObjectMarker::setSubMode(const int subMode) + { + if (subMode == mSubMode) + return; + mSubMode = subMode; + resetMarkerHighlight(); + updateSelectionMarker(); + } + + bool ObjectMarker::hitBehindMarker(const osg::Vec3d& hitPos, osg::ref_ptr camera) + { + if (mSubMode != Object::Mode_Rotate) + return false; + + osg::Vec3d center, eye, forwardVector, _; + std::vector rotMark = mMarkerNodes["rotateMarkers"]->getParentalNodePaths()[0]; + const osg::Vec3f markerPos = osg::computeLocalToWorld(rotMark).getTrans(); + + camera->getViewMatrixAsLookAt(eye, center, _); + forwardVector = center - eye; + forwardVector.normalize(); + + return (hitPos - markerPos) * forwardVector > 0; + } + + bool ObjectMarker::attachMarker(const std::string& refId) + { + const auto& object = mWorldspaceWidget->getObjectByReferenceId(refId); + + if (!object) + removeFromSelectionHistory(refId); + + if (!object || !object->getSelected()) + return false; + + if (!object->getRootNode()->addChild(mRootNode)) + throw std::runtime_error("Failed to add marker to object"); + + std::string parentMarkerNode; + + switch (mSubMode) + { + case (Object::Mode_Rotate): + parentMarkerNode = "rotateMarkers"; + addTagToActiveMarkerNodes(mMarkerNodes, object, { "_Axis_Rot" }); + break; + case (Object::Mode_Scale): + parentMarkerNode = "scaleMarkers"; + addTagToActiveMarkerNodes(mMarkerNodes, object, { "_Axis_Scale", "_Wall_Scale" }); + break; + case (Object::Mode_Move): + default: + parentMarkerNode = "moveMarkers"; + addTagToActiveMarkerNodes(mMarkerNodes, object, { "_Axis", "_Wall" }); + break; + } + + mMarkerNodes[parentMarkerNode]->asGroup()->setNodeMask(Mask_Reference); + + return true; + } + + void ObjectMarker::detachMarker() + { + for (std::size_t index = mRootNode->getNumParents(); index > 0;) + mRootNode->getParent(--index)->removeChild(mRootNode); + + osg::ref_ptr widgetRoot = mMarkerNodes["unitArrows"]->asGroup(); + for (std::size_t index = widgetRoot->getNumChildren(); index > 0;) + widgetRoot->getChild(--index)->setNodeMask(Mask_Hidden); + } + + void ObjectMarker::addToSelectionHistory(const std::string& refId, bool update) + { + auto foundObject = std::find_if(mSelectionHistory.begin(), mSelectionHistory.end(), + [&refId](const std::string& objId) { return objId == refId; }); + + if (foundObject == mSelectionHistory.end()) + mSelectionHistory.push_back(refId); + else + std::rotate(foundObject, foundObject + 1, mSelectionHistory.end()); + + if (update) + updateSelectionMarker(refId); + } + + void ObjectMarker::removeFromSelectionHistory(const std::string& refId) + { + mSelectionHistory.erase(std::remove_if(mSelectionHistory.begin(), mSelectionHistory.end(), + [&refId](const std::string& objId) { return objId == refId; }), + mSelectionHistory.end()); + } + + void ObjectMarker::updateSelectionMarker(const std::string& refId) + { + if (mSelectionHistory.empty()) + return; + + detachMarker(); + + if (refId.empty()) + { + for (std::size_t index = mSelectionHistory.size(); index > 0;) + if (attachMarker(mSelectionHistory[--index])) + break; + } + else + attachMarker(refId); + } + + void ObjectMarker::resetMarkerHighlight() + { + if (mLastHighlightedNodes.empty()) + return; + + for (const auto& [nodeName, mat] : mLastHighlightedNodes) + mat->setEmission(osg::Material::FRONT_AND_BACK, mat->getEmission(osg::Material::FRONT_AND_BACK) / 4); + + mLastHighlightedNodes.clear(); + mLastHitNode.clear(); + } + + void ObjectMarker::updateMarkerHighlight(const std::string_view hitNode, const int axis) + { + if (hitNode == mLastHitNode) + return; + + resetMarkerHighlight(); + + std::string colorName; + + switch (axis) + { + case Object::Axis_X: + colorName = "red"; + break; + case Object::Axis_Y: + colorName = "green"; + break; + case Object::Axis_Z: + colorName = "blue"; + break; + default: + throw std::runtime_error("Invalid axis for highlighting: " + std::to_string(axis)); + } + + std::vector targetMaterials = { colorName + "-material" }; + + if (mSubMode != Object::Mode_Rotate) + targetMaterials.emplace_back(colorName + "_alpha-material"); + + for (const auto& materialNodeName : targetMaterials) + { + osg::ref_ptr matNode = mMarkerNodes[materialNodeName]; + osg::StateSet* state = matNode->getStateSet(); + osg::StateAttribute* matAttr = state->getAttribute(osg::StateAttribute::MATERIAL); + + osg::Material* mat = static_cast(matAttr); + mat->setEmission(osg::Material::FRONT_AND_BACK, mOriginalColors[materialNodeName]); + + mLastHighlightedNodes.emplace(std::make_pair(matNode->getName(), mat)); + } + + mLastHitNode = hitNode; + } +} diff --git a/apps/opencs/view/render/objectmarker.hpp b/apps/opencs/view/render/objectmarker.hpp new file mode 100644 index 0000000000..483d6f6be6 --- /dev/null +++ b/apps/opencs/view/render/objectmarker.hpp @@ -0,0 +1,77 @@ +#ifndef OPENCS_VIEW_OBJECT_MARKER_H +#define OPENCS_VIEW_OBJECT_MARKER_H + +#include "object.hpp" + +namespace osg +{ + class Camera; + class Material; +} + +namespace CSVRender +{ + using NodeMap = std::unordered_map>; + class WorldspaceWidget; + + class ObjectMarkerTag : public ObjectTag + { + public: + ObjectMarkerTag(Object* object, int axis); + + int mAxis; + }; + + class ObjectMarker + { + friend class WorldspaceWidget; + + WorldspaceWidget* mWorldspaceWidget; + Resource::ResourceSystem* mResourceSystem; + NodeMap mMarkerNodes; + osg::ref_ptr mBaseNode; + osg::ref_ptr mRootNode; + std::unordered_map mOriginalColors; + std::vector mSelectionHistory; + std::string mLastHitNode; + std::unordered_map mLastHighlightedNodes; + float mMarkerScale; + int mSubMode; + + ObjectMarker(WorldspaceWidget* worldspaceWidget, Resource::ResourceSystem* resourceSystem); + + static std::unique_ptr create(WorldspaceWidget* widget, Resource::ResourceSystem* resourceSystem) + { + return std::unique_ptr(new ObjectMarker(widget, resourceSystem)); + } + + bool attachMarker(const std::string& refId); + + void removeFromSelectionHistory(const std::string& refId); + + public: + ObjectMarker(ObjectMarker&) = delete; + ObjectMarker(ObjectMarker&&) = delete; + ObjectMarker& operator=(const ObjectMarker&) = delete; + ObjectMarker& operator=(ObjectMarker&&) = delete; + + void toggleVisibility(); + + bool hitBehindMarker(const osg::Vec3d& hitPos, osg::ref_ptr camera); + + void detachMarker(); + + void addToSelectionHistory(const std::string& refId, bool update = true); + + void updateSelectionMarker(const std::string& refId = std::string()); + + void resetMarkerHighlight(); + + void updateMarkerHighlight(const std::string_view hitNode, const int axis); + + void setSubMode(const int subMode); + + void updateScale(const float scale); + }; +} +#endif // OPENCS_VIEW_OBJECT_MARKER_H diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp index 3fd35b7740..90670a4d62 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.cpp +++ b/apps/opencs/view/render/pagedworldspacewidget.cpp @@ -86,8 +86,8 @@ bool CSVRender::PagedWorldspaceWidget::adjustCells() { modified = true; - auto cell - = std::make_unique(mDocument, mRootNode, iter->first.getId(mWorldspace), deleted, true); + auto cell = std::make_unique(getDocument(), mSelectionMarker.get(), mRootNode, + iter->first.getId(mWorldspace), deleted, true); delete iter->second; iter->second = cell.release(); @@ -465,7 +465,8 @@ void CSVRender::PagedWorldspaceWidget::addCellToScene(const CSMWorld::CellCoordi bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; - auto cell = std::make_unique(mDocument, mRootNode, coordinates.getId(mWorldspace), deleted, true); + auto cell = std::make_unique( + getDocument(), mSelectionMarker.get(), mRootNode, coordinates.getId(mWorldspace), deleted, true); EditMode* editMode = getEditMode(); cell->setSubMode(editMode->getSubMode(), editMode->getInteractionMask()); @@ -750,6 +751,7 @@ void CSVRender::PagedWorldspaceWidget::clearSelection(int elementMask) iter->second->setSelection(elementMask, Cell::Selection_Clear); flagAsModified(); + mSelectionMarker->detachMarker(); } void CSVRender::PagedWorldspaceWidget::invertSelection(int elementMask) @@ -907,6 +909,7 @@ void CSVRender::PagedWorldspaceWidget::setSubMode(int subMode, unsigned int elem { for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) iter->second->setSubMode(subMode, elementMask); + mSelectionMarker->updateSelectionMarker(); } void CSVRender::PagedWorldspaceWidget::reset(unsigned int elementMask) @@ -986,3 +989,12 @@ void CSVRender::PagedWorldspaceWidget::loadSouthCell() { addCellToSceneFromCamera(0, -1); } + +CSVRender::Object* CSVRender::PagedWorldspaceWidget::getObjectByReferenceId(const std::string& referenceId) +{ + for (const auto& [_, cell] : mCells) + if (const auto& object = cell->getObjectByReferenceId(referenceId)) + return object; + + return nullptr; +} diff --git a/apps/opencs/view/render/pagedworldspacewidget.hpp b/apps/opencs/view/render/pagedworldspacewidget.hpp index 744cc7ccb9..dc47d5ea04 100644 --- a/apps/opencs/view/render/pagedworldspacewidget.hpp +++ b/apps/opencs/view/render/pagedworldspacewidget.hpp @@ -174,6 +174,8 @@ namespace CSVRender /// Erase all overrides and restore the visual representation to its true state. void reset(unsigned int elementMask) override; + CSVRender::Object* getObjectByReferenceId(const std::string& referenceId) override; + protected: void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) override; diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 716a087d02..5e8db03cf8 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -161,8 +161,6 @@ namespace CSVRender , mLighting(nullptr) , mHasDefaultAmbient(false) , mIsExterior(true) - , mPrevMouseX(0) - , mPrevMouseY(0) , mCamPositionSet(false) { mFreeCamControl = new FreeCameraController(this); @@ -423,10 +421,10 @@ namespace CSVRender void SceneWidget::mouseMoveEvent(QMouseEvent* event) { - mCurrentCamControl->handleMouseMoveEvent(event->x() - mPrevMouseX, event->y() - mPrevMouseY); + QPointF pos = event->position(); + mCurrentCamControl->handleMouseMoveEvent(pos.x() - mPrevMouse.x(), pos.y() - mPrevMouse.y()); - mPrevMouseX = event->x(); - mPrevMouseY = event->y(); + mPrevMouse = pos; } void SceneWidget::wheelEvent(QWheelEvent* event) @@ -445,6 +443,32 @@ namespace CSVRender mCurrentCamControl->setup(mRootNode, Mask_Reference | Mask_Terrain, CameraController::WorldUp); mCamPositionSet = true; } + + if (mSelectionMarkerNode) + { + osg::MatrixList worldMats = mSelectionMarkerNode->getWorldMatrices(); + if (!worldMats.empty()) + { + osg::Matrixd markerWorldMat = worldMats[0]; + + osg::Vec3f eye, _; + mView->getCamera()->getViewMatrix().getLookAt(eye, _, _); + osg::Vec3f cameraLocalPos = eye * osg::Matrixd::inverse(markerWorldMat); + + bool isInFrontRightQuadrant = (cameraLocalPos.x() > 0.1f) && (cameraLocalPos.y() > 0.1f); + bool isSignificantlyBehind = (cameraLocalPos.x() < 1.f) && (cameraLocalPos.y() < 1.f); + + if (!isInFrontRightQuadrant && isSignificantlyBehind) + { + osg::Quat current = mSelectionMarkerNode->getAttitude(); + mSelectionMarkerNode->setAttitude(current * osg::Quat(osg::PI, osg::Vec3f(0, 0, 1))); + } + + float distance = (markerWorldMat.getTrans() - eye).length(); + float scale = std::max(distance / 75.0f, 1.0f); + mSelectionMarkerNode->setScale(osg::Vec3(scale, scale, scale)); + } + } } void SceneWidget::settingChanged(const CSMPrefs::Setting* setting) diff --git a/apps/opencs/view/render/scenewidget.hpp b/apps/opencs/view/render/scenewidget.hpp index 228581a0ef..2e9526eff6 100644 --- a/apps/opencs/view/render/scenewidget.hpp +++ b/apps/opencs/view/render/scenewidget.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -105,6 +106,11 @@ namespace CSVRender void setExterior(bool isExterior); + void setSelectionMarkerRoot(osg::ref_ptr selectionMarker) + { + mSelectionMarkerNode = selectionMarker; + } + protected: void setLighting(Lighting* lighting); ///< \attention The ownership of \a lighting is not transferred to *this. @@ -122,6 +128,7 @@ namespace CSVRender Lighting* mLighting; + osg::ref_ptr mSelectionMarkerNode; osg::ref_ptr mGradientCamera; osg::Vec4f mDefaultAmbient; bool mHasDefaultAmbient; @@ -130,7 +137,7 @@ namespace CSVRender LightingNight mLightingNight; LightingBright mLightingBright; - int mPrevMouseX, mPrevMouseY; + QPointF mPrevMouse; /// Tells update that camera isn't set bool mCamPositionSet; diff --git a/apps/opencs/view/render/terrainshapemode.cpp b/apps/opencs/view/render/terrainshapemode.cpp index bc1c6c6365..4f8bd44f05 100644 --- a/apps/opencs/view/render/terrainshapemode.cpp +++ b/apps/opencs/view/render/terrainshapemode.cpp @@ -1661,7 +1661,7 @@ void CSVRender::TerrainShapeMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask()); if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) diff --git a/apps/opencs/view/render/terraintexturemode.cpp b/apps/opencs/view/render/terraintexturemode.cpp index cfc7f50cf1..b3fd64705d 100644 --- a/apps/opencs/view/render/terraintexturemode.cpp +++ b/apps/opencs/view/render/terraintexturemode.cpp @@ -724,7 +724,7 @@ void CSVRender::TerrainTextureMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event) { - WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); + WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->position().toPoint(), getInteractionMask()); if (hit.hit && mBrushDraw) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw) diff --git a/apps/opencs/view/render/unpagedworldspacewidget.cpp b/apps/opencs/view/render/unpagedworldspacewidget.cpp index a7d8af0a62..3637663356 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.cpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.cpp @@ -79,7 +79,7 @@ CSVRender::UnpagedWorldspaceWidget::UnpagedWorldspaceWidget( update(); - mCell = std::make_unique(document, mRootNode, mCellId); + mCell = std::make_unique(document, mSelectionMarker.get(), mRootNode, mCellId); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) @@ -127,7 +127,7 @@ bool CSVRender::UnpagedWorldspaceWidget::handleDrop( mCellId = universalIdData.begin()->getId(); - mCell = std::make_unique(getDocument(), mRootNode, mCellId); + mCell = std::make_unique(getDocument(), mSelectionMarker.get(), mRootNode, mCellId); mCamPositionSet = false; mOrbitCamControl->reset(); @@ -141,6 +141,7 @@ void CSVRender::UnpagedWorldspaceWidget::clearSelection(int elementMask) { mCell->setSelection(elementMask, Cell::Selection_Clear); flagAsModified(); + mSelectionMarker->detachMarker(); } void CSVRender::UnpagedWorldspaceWidget::invertSelection(int elementMask) @@ -218,6 +219,7 @@ std::vector> CSVRender::UnpagedWorldspaceWidget void CSVRender::UnpagedWorldspaceWidget::setSubMode(int subMode, unsigned int elementMask) { mCell->setSubMode(subMode, elementMask); + mSelectionMarker->updateSelectionMarker(); } void CSVRender::UnpagedWorldspaceWidget::reset(unsigned int elementMask) @@ -383,3 +385,8 @@ CSVRender::WorldspaceWidget::dropRequirments CSVRender::UnpagedWorldspaceWidget: return ignored; } } + +CSVRender::Object* CSVRender::UnpagedWorldspaceWidget::getObjectByReferenceId(const std::string& referenceId) +{ + return mCell->getObjectByReferenceId(referenceId); +} diff --git a/apps/opencs/view/render/unpagedworldspacewidget.hpp b/apps/opencs/view/render/unpagedworldspacewidget.hpp index 89c916415d..6eb5b97f56 100644 --- a/apps/opencs/view/render/unpagedworldspacewidget.hpp +++ b/apps/opencs/view/render/unpagedworldspacewidget.hpp @@ -104,6 +104,8 @@ namespace CSVRender /// Erase all overrides and restore the visual representation to its true state. void reset(unsigned int elementMask) override; + CSVRender::Object* getObjectByReferenceId(const std::string& id) override; + private: void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 0a02ae456b..836f5ff0ea 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -52,7 +52,6 @@ #include "cameracontroller.hpp" #include "instancemode.hpp" #include "mask.hpp" -#include "object.hpp" #include "pathgridmode.hpp" CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidget* parent) @@ -74,8 +73,8 @@ CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidge , mToolTipPos(-1, -1) , mShowToolTips(false) , mToolTipDelay(0) - , mInConstructor(true) , mSelectedNavigationMode(0) + , mSelectionMarker(ObjectMarker::create(this, document.getData().getResourceSystem().get())) { setAcceptDrops(true); @@ -145,13 +144,14 @@ CSVRender::WorldspaceWidget::WorldspaceWidget(CSMDoc::Document& document, QWidge &WorldspaceWidget::unhideAll); connect(new CSMPrefs::Shortcut("scene-clear-selection", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, - [this] { this->clearSelection(Mask_Reference); }); + [this] { clearSelection(Mask_Reference); }); CSMPrefs::Shortcut* switchPerspectiveShortcut = new CSMPrefs::Shortcut("scene-cam-cycle", this); connect(switchPerspectiveShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::cycleNavigationMode); - mInConstructor = false; + connect(new CSMPrefs::Shortcut("scene-toggle-marker", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, + [this]() { mSelectionMarker->toggleVisibility(); }); } void CSVRender::WorldspaceWidget::settingChanged(const CSMPrefs::Setting* setting) @@ -162,17 +162,8 @@ void CSVRender::WorldspaceWidget::settingChanged(const CSMPrefs::Setting* settin mDragWheelFactor = setting->toDouble(); else if (*setting == "3D Scene Input/drag-shift-factor") mDragShiftFactor = setting->toDouble(); - else if (*setting == "Rendering/object-marker-alpha" && !mInConstructor) - { - float alpha = setting->toDouble(); - // getSelection is virtual, thus this can not be called from the constructor - auto selection = getSelection(Mask_Reference); - for (osg::ref_ptr tag : selection) - { - if (auto objTag = dynamic_cast(tag.get())) - objTag->mObject->setMarkerTransparency(alpha); - } - } + else if (*setting == "Rendering/object-marker-scale") + mSelectionMarker->updateScale(setting->toDouble()); else if (*setting == "Tooltips/scene-delay") mToolTipDelay = setting->toInt(); else if (*setting == "Tooltips/scene") @@ -396,8 +387,29 @@ CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() return mDocument; } -CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( - const QPoint& localPos, unsigned int interactionMask) const +template +std::optional CSVRender::WorldspaceWidget::checkTag( + const osgUtil::LineSegmentIntersector::Intersection& intersection) const +{ + for (auto* node : intersection.nodePath) + { + if (auto* tag = dynamic_cast(node->getUserData())) + { + WorldspaceHitResult hit = { true, tag, 0, 0, 0, intersection.getWorldIntersectPoint() }; + if (intersection.indexList.size() >= 3) + { + hit.index0 = intersection.indexList[0]; + hit.index1 = intersection.indexList[1]; + hit.index2 = intersection.indexList[2]; + } + return hit; + } + } + return std::nullopt; +} + +std::tuple CSVRender::WorldspaceWidget::getStartEndDirection( + int pointX, int pointY) const { // may be okay to just use devicePixelRatio() directly QScreen* screen = SceneWidget::windowHandle() && SceneWidget::windowHandle()->screen() @@ -405,8 +417,8 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( : QGuiApplication::primaryScreen(); // (0,0) is considered the lower left corner of an OpenGL window - int x = localPos.x() * screen->devicePixelRatio(); - int y = height() * screen->devicePixelRatio() - localPos.y() * screen->devicePixelRatio(); + int x = pointX * screen->devicePixelRatio(); + int y = height() * screen->devicePixelRatio() - pointY * screen->devicePixelRatio(); // Convert from screen space to world space osg::Matrixd wpvMat; @@ -418,6 +430,13 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( osg::Vec3d start = wpvMat.preMult(osg::Vec3d(x, y, 0)); osg::Vec3d end = wpvMat.preMult(osg::Vec3d(x, y, 1)); osg::Vec3d direction = end - start; + return { start, end, direction }; +} + +CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( + const QPoint& localPos, unsigned int interactionMask) const +{ + auto [start, end, direction] = getStartEndDirection(localPos.x(), localPos.y()); // Get intersection osg::ref_ptr intersector( @@ -430,51 +449,46 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick( mView->getCamera()->accept(visitor); - // Get relevant data - for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); - it != intersector->getIntersections().end(); ++it) - { - osgUtil::LineSegmentIntersector::Intersection intersection = *it; + auto intersections = intersector->getIntersections(); - // reject back-facing polygons - if (direction * intersection.getWorldIntersectNormal() > 0) - { - continue; - } + std::vector validIntersections + = { intersections.begin(), intersections.end() }; - for (std::vector::iterator nodeIter = intersection.nodePath.begin(); - nodeIter != intersection.nodePath.end(); ++nodeIter) - { - osg::Node* node = *nodeIter; - if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) - { - WorldspaceHitResult hit = { true, std::move(tag), 0, 0, 0, intersection.getWorldIntersectPoint() }; - if (intersection.indexList.size() >= 3) - { - hit.index0 = intersection.indexList[0]; - hit.index1 = intersection.indexList[1]; - hit.index2 = intersection.indexList[2]; - } - return hit; - } - } + const auto& removeBackfaces = [direction = direction](const osgUtil::LineSegmentIntersector::Intersection& i) { + return direction * i.getWorldIntersectNormal() > 0; + }; - // Something untagged, probably terrain - WorldspaceHitResult hit = { true, nullptr, 0, 0, 0, intersection.getWorldIntersectPoint() }; - if (intersection.indexList.size() >= 3) - { - hit.index0 = intersection.indexList[0]; - hit.index1 = intersection.indexList[1]; - hit.index2 = intersection.indexList[2]; - } - return hit; - } + validIntersections.erase(std::remove_if(validIntersections.begin(), validIntersections.end(), removeBackfaces), + validIntersections.end()); // Default placement direction.normalize(); direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt(); - WorldspaceHitResult hit = { false, nullptr, 0, 0, 0, start + direction }; + if (validIntersections.empty()) + return WorldspaceHitResult{ false, nullptr, 0, 0, 0, start + direction }; + + const auto& firstHit = validIntersections.front(); + + for (const auto& hit : validIntersections) + if (const auto& markerHit = checkTag(hit)) + { + if (mSelectionMarker->hitBehindMarker(markerHit->worldPos, mView->getCamera())) + return WorldspaceHitResult{ false, nullptr, 0, 0, 0, start + direction }; + else + return *markerHit; + } + if (auto hit = checkTag(firstHit)) + return *hit; + + // Something untagged, probably terrain + WorldspaceHitResult hit = { true, nullptr, 0, 0, 0, firstHit.getWorldIntersectPoint() }; + if (firstHit.indexList.size() >= 3) + { + hit.index0 = firstHit.indexList[0]; + hit.index1 = firstHit.indexList[1]; + hit.index2 = firstHit.indexList[2]; + } return hit; } @@ -632,17 +646,53 @@ void CSVRender::WorldspaceWidget::elementSelectionChanged() void CSVRender::WorldspaceWidget::updateOverlay() {} +void CSVRender::WorldspaceWidget::handleMarkerHighlight(const int x, const int y) +{ + auto [start, end, _] = getStartEndDirection(x, y); + + osg::ref_ptr intersector( + new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); + + intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); + osgUtil::IntersectionVisitor visitor(intersector); + + visitor.setTraversalMask(Mask_Reference); + + mView->getCamera()->accept(visitor); + + bool hitMarker = false; + for (const auto& intersection : intersector->getIntersections()) + { + if (mSelectionMarker->hitBehindMarker(intersection.getWorldIntersectPoint(), mView->getCamera())) + continue; + + for (const auto& node : intersection.nodePath) + { + if (const auto& marker = dynamic_cast(node->getUserData())) + { + hitMarker = true; + mSelectionMarker->updateMarkerHighlight(node->getName(), marker->mAxis); + break; + } + } + } + + if (!hitMarker) + mSelectionMarker->resetMarkerHighlight(); +} + void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) { dynamic_cast(*mEditMode->getCurrent()).mouseMoveEvent(event); if (mDragging) { - int diffX = event->x() - mDragX; - int diffY = (height() - event->y()) - mDragY; + QPoint pos = event->position().toPoint(); + int diffX = pos.x() - mDragX; + int diffY = (height() - pos.y()) - mDragY; - mDragX = event->x(); - mDragY = height() - event->y(); + mDragX = pos.x(); + mDragY = height() - pos.y(); double factor = mDragFactor; @@ -651,32 +701,32 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); - editMode.drag(event->pos(), diffX, diffY, factor); + editMode.drag(event->position().toPoint(), diffX, diffY, factor); } else if (mDragMode != InteractionType_None) { EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (mDragMode == InteractionType_PrimaryEdit) - mDragging = editMode.primaryEditStartDrag(event->pos()); + mDragging = editMode.primaryEditStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_SecondaryEdit) - mDragging = editMode.secondaryEditStartDrag(event->pos()); + mDragging = editMode.secondaryEditStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_PrimarySelect) - mDragging = editMode.primarySelectStartDrag(event->pos()); + mDragging = editMode.primarySelectStartDrag(event->position().toPoint()); else if (mDragMode == InteractionType_SecondarySelect) - mDragging = editMode.secondarySelectStartDrag(event->pos()); + mDragging = editMode.secondarySelectStartDrag(event->position().toPoint()); if (mDragging) { - mDragX = event->localPos().x(); - mDragY = height() - event->localPos().y(); + mDragX = event->position().x(); + mDragY = height() - event->position().y(); } } else { - if (event->globalPos() != mToolTipPos) + if (event->globalPosition().toPoint() != mToolTipPos) { - mToolTipPos = event->globalPos(); + mToolTipPos = event->globalPosition().toPoint(); if (mShowToolTips) { @@ -685,6 +735,8 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) } } + QPoint pos = event->position().toPoint(); + handleMarkerHighlight(pos.x(), pos.y()); SceneWidget::mouseMoveEvent(event); } } diff --git a/apps/opencs/view/render/worldspacewidget.hpp b/apps/opencs/view/render/worldspacewidget.hpp index 9a7df38620..831ed13640 100644 --- a/apps/opencs/view/render/worldspacewidget.hpp +++ b/apps/opencs/view/render/worldspacewidget.hpp @@ -13,6 +13,7 @@ #include #include "instancedragmodes.hpp" +#include "objectmarker.hpp" #include "scenewidget.hpp" class QDragEnterEvent; @@ -89,7 +90,6 @@ namespace CSVRender QPoint mToolTipPos; bool mShowToolTips; int mToolTipDelay; - bool mInConstructor; int mSelectedNavigationMode; public: @@ -186,6 +186,12 @@ namespace CSVRender virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0; + template + std::optional checkTag( + const osgUtil::LineSegmentIntersector::Intersection& intersection) const; + + std::tuple getStartEndDirection(int pointX, int pointY) const; + /// Return the next intersection with scene elements matched by /// \a interactionMask based on \a localPos and the camera vector. /// If there is no such intersection, instead a point "in front" of \a localPos will be @@ -216,7 +222,14 @@ namespace CSVRender EditMode* getEditMode(); + virtual CSVRender::Object* getObjectByReferenceId(const std::string& id) = 0; + + ObjectMarker* getSelectionMarker() { return mSelectionMarker.get(); } + const ObjectMarker* getSelectionMarker() const { return mSelectionMarker.get(); } + protected: + const std::unique_ptr mSelectionMarker; + /// Visual elements in a scene /// @note do not change the enumeration values, they are used in pre-existing button file names! enum ButtonId @@ -247,11 +260,13 @@ namespace CSVRender void settingChanged(const CSMPrefs::Setting* setting) override; - bool getSpeedMode(); - void cycleNavigationMode(); private: + bool hitBehindMarker(const osg::Vec3d& hitPos) const; + + void handleMarkerHighlight(const int x, const int y); + void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; diff --git a/apps/opencs/view/tools/reporttable.cpp b/apps/opencs/view/tools/reporttable.cpp index 7ec55b96b5..8c92d9980f 100644 --- a/apps/opencs/view/tools/reporttable.cpp +++ b/apps/opencs/view/tools/reporttable.cpp @@ -93,7 +93,7 @@ void CSVTools::ReportTable::contextMenuEvent(QContextMenuEvent* event) void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event) diff --git a/apps/opencs/view/widget/colorpickerpopup.cpp b/apps/opencs/view/widget/colorpickerpopup.cpp index 87c62e137b..0918e798a5 100644 --- a/apps/opencs/view/widget/colorpickerpopup.cpp +++ b/apps/opencs/view/widget/colorpickerpopup.cpp @@ -52,7 +52,7 @@ void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent* event) // If the mouse is pressed above the pop-up parent, // the pop-up will be hidden and the pressed signal won't be repeated for the parent - if (buttonRect.contains(event->globalPos()) || buttonRect.contains(event->pos())) + if (buttonRect.contains(event->globalPosition().toPoint()) || buttonRect.contains(event->position().toPoint())) { setAttribute(Qt::WA_NoMouseReplay); } diff --git a/apps/opencs/view/widget/completerpopup.cpp b/apps/opencs/view/widget/completerpopup.cpp index 91b0902659..25daee1f1f 100644 --- a/apps/opencs/view/widget/completerpopup.cpp +++ b/apps/opencs/view/widget/completerpopup.cpp @@ -22,12 +22,8 @@ int CSVWidget::CompleterPopup::sizeHintForRow(int row) const ensurePolished(); QModelIndex index = model()->index(row, modelColumn()); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif - QAbstractItemDelegate* delegate = itemDelegate(index); + QAbstractItemDelegate* delegate = itemDelegateForIndex(index); return delegate->sizeHint(option, index).height(); } diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index 168e555eae..ac69805134 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -85,7 +85,7 @@ void CSVWorld::NotEditableSubDelegate::setEditorData(QWidget* editor, const QMod CSMWorld::Columns::ColumnId columnId = static_cast(mTable->getColumnId(index.column())); - if (QVariant::String == v.type()) + if (QMetaType::QString == v.typeId()) { label->setText(v.toString()); } diff --git a/apps/opencs/view/world/dragrecordtable.cpp b/apps/opencs/view/world/dragrecordtable.cpp index a006779ba4..adff809335 100644 --- a/apps/opencs/view/world/dragrecordtable.cpp +++ b/apps/opencs/view/world/dragrecordtable.cpp @@ -55,7 +55,7 @@ void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent* event) void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index))) @@ -71,7 +71,7 @@ void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) void CSVWorld::DragRecordTable::dropEvent(QDropEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); CSMWorld::ColumnBase::Display display = getIndexDisplayType(index); if (CSVWorld::DragDropUtils::canAcceptData(*event, display)) { diff --git a/apps/opencs/view/world/regionmap.cpp b/apps/opencs/view/world/regionmap.cpp index 17d0016afc..ae9ac94022 100644 --- a/apps/opencs/view/world/regionmap.cpp +++ b/apps/opencs/view/world/regionmap.cpp @@ -341,7 +341,7 @@ void CSVWorld::RegionMap::viewInTable() void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event) { - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } std::vector CSVWorld::RegionMap::getDraggedRecords() const @@ -376,7 +376,7 @@ void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event) void CSVWorld::RegionMap::dropEvent(QDropEvent* event) { - QModelIndex index = indexAt(event->pos()); + QModelIndex index = indexAt(event->position().toPoint()); bool exists = QTableView::model()->data(index, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); if (!index.isValid() || !exists) diff --git a/apps/opencs/view/world/scriptedit.cpp b/apps/opencs/view/world/scriptedit.cpp index 00acf71235..a846d4b342 100644 --- a/apps/opencs/view/world/scriptedit.cpp +++ b/apps/opencs/view/world/scriptedit.cpp @@ -21,6 +21,24 @@ #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" +namespace +{ + void prependToEachLine(QTextCursor begin, const QString& text) + { + QTextCursor end = begin; + begin.setPosition(begin.selectionStart()); + begin.movePosition(QTextCursor::StartOfLine); + end.setPosition(end.selectionEnd()); + end.movePosition(QTextCursor::EndOfLine); + begin.beginEditBlock(); + for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) + { + begin.insertText(text); + } + begin.endEditBlock(); + } +} + CSVWorld::ScriptEdit::ChangeLock::ChangeLock(ScriptEdit& edit) : mEdit(edit) { @@ -46,6 +64,55 @@ bool CSVWorld::ScriptEdit::event(QEvent* event) return QPlainTextEdit::event(event); } +void CSVWorld::ScriptEdit::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Backtab) + { + QTextCursor cursor = textCursor(); + QTextCursor end = cursor; + cursor.setPosition(cursor.selectionStart()); + cursor.movePosition(QTextCursor::StartOfLine); + end.setPosition(end.selectionEnd()); + end.movePosition(QTextCursor::EndOfLine); + cursor.beginEditBlock(); + for (; cursor < end; cursor.movePosition(QTextCursor::EndOfLine), cursor.movePosition(QTextCursor::Right)) + { + cursor.select(QTextCursor::LineUnderCursor); + QString line = cursor.selectedText(); + + if (line.isEmpty()) + continue; + qsizetype index = 0; + if (line[0] == '\t') + index = 1; + else + { + // Remove up to a tab worth of spaces instead + while (line[index].isSpace() && index < mTabCharCount && line[index] != '\t') + index++; + } + + if (index != 0) + { + line.remove(0, index); + cursor.insertText(line); + } + } + cursor.endEditBlock(); + return; + } + else if (event->key() == Qt::Key_Tab) + { + QTextCursor cursor = textCursor(); + if (cursor.hasSelection()) + { + prependToEachLine(cursor, "\t"); + return; + } + } + QPlainTextEdit::keyPressEvent(event); +} + CSVWorld::ScriptEdit::ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent) : QPlainTextEdit(parent) , mChangeLocked(0) @@ -136,7 +203,7 @@ void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event) QPlainTextEdit::dragEnterEvent(event); else { - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); event->acceptProposedAction(); } } @@ -148,7 +215,7 @@ void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event) QPlainTextEdit::dragMoveEvent(event); else { - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); event->accept(); } } @@ -162,7 +229,7 @@ void CSVWorld::ScriptEdit::dropEvent(QDropEvent* event) return; } - setTextCursor(cursorForPosition(event->pos())); + setTextCursor(cursorForPosition(event->position().toPoint())); if (mime->fromDocument(mDocument)) { @@ -316,22 +383,7 @@ void CSVWorld::ScriptEdit::markOccurrences() void CSVWorld::ScriptEdit::commentSelection() { - QTextCursor begin = textCursor(); - QTextCursor end = begin; - begin.setPosition(begin.selectionStart()); - begin.movePosition(QTextCursor::StartOfLine); - - end.setPosition(end.selectionEnd()); - end.movePosition(QTextCursor::EndOfLine); - - begin.beginEditBlock(); - - for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) - { - begin.insertText(";"); - } - - begin.endEditBlock(); + prependToEachLine(textCursor(), ";"); } void CSVWorld::ScriptEdit::uncommentSelection() @@ -345,17 +397,16 @@ void CSVWorld::ScriptEdit::uncommentSelection() end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); - for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText(); - if (line.size() == 0) + if (line.isEmpty()) continue; // get first nonspace character in line - int index; + qsizetype index; for (index = 0; index != line.size(); ++index) { if (!line[index].isSpace()) diff --git a/apps/opencs/view/world/scriptedit.hpp b/apps/opencs/view/world/scriptedit.hpp index 53fa88ced3..d44c29eaab 100644 --- a/apps/opencs/view/world/scriptedit.hpp +++ b/apps/opencs/view/world/scriptedit.hpp @@ -74,6 +74,7 @@ namespace CSVWorld protected: bool event(QEvent* event) override; + void keyPressEvent(QKeyEvent* e) override; public: ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent); diff --git a/apps/opencs/view/world/table.cpp b/apps/opencs/view/world/table.cpp index 86a1e93bbd..2cadbf289a 100644 --- a/apps/opencs/view/world/table.cpp +++ b/apps/opencs/view/world/table.cpp @@ -592,7 +592,7 @@ void CSVWorld::Table::moveRecords(QDropEvent* event) if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; - QModelIndex targedIndex = indexAt(event->pos()); + QModelIndex targedIndex = indexAt(event->position().toPoint()); QModelIndexList selectedRows = selectionModel()->selectedRows(); int targetRowRaw = targedIndex.row(); @@ -872,7 +872,7 @@ void CSVWorld::Table::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { - startDragFromTable(*this, indexAt(event->pos())); + startDragFromTable(*this, indexAt(event->position().toPoint())); } } diff --git a/apps/opencs/view/world/tableheadermouseeventhandler.cpp b/apps/opencs/view/world/tableheadermouseeventhandler.cpp index dcd2e659a6..2bc6c4fe00 100644 --- a/apps/opencs/view/world/tableheadermouseeventhandler.cpp +++ b/apps/opencs/view/world/tableheadermouseeventhandler.cpp @@ -31,7 +31,7 @@ namespace CSVWorld auto& clickEvent = static_cast(*event); if ((clickEvent.button() == Qt::MiddleButton)) { - const auto& index = table.indexAt(clickEvent.pos()); + const auto& index = table.indexAt(clickEvent.position().toPoint()); table.setColumnHidden(index.column(), true); clickEvent.accept(); return true; diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index dfb587cd96..39b8ba321e 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -171,7 +171,7 @@ QWidget* CSVWorld::CommandDelegate::createEditor( // TODO: Find a better solution? if (display == CSMWorld::ColumnBase::Display_Boolean) { - return QItemEditorFactory::defaultFactory()->createEditor(QVariant::Bool, parent); + return QItemEditorFactory::defaultFactory()->createEditor(QMetaType::Bool, parent); } // For tables the pop-up of the color editor should appear immediately after the editor creation // (the third parameter of ColorEditor's constructor) @@ -362,11 +362,7 @@ void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex if (!n.isEmpty()) { if (!variant.isValid()) -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) variant = QVariant(editor->property(n).metaType(), (const void*)nullptr); -#else - variant = QVariant(editor->property(n).userType(), (const void*)nullptr); -#endif editor->setProperty(n, variant); } } diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index e3629d6f27..2a5ad2d18d 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -44,7 +44,7 @@ add_openmw_dir (mwgui tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog recharge mode videowidget backgroundimage itemwidget screenfader debugwindow spellmodel spellview draganddrop timeadvancer jailscreen itemchargeview keyboardnavigation textcolours statswatcher - postprocessorhud settings + postprocessorhud settings worlditemmodel itemtransfer ) add_openmw_dir (mwdialogue @@ -63,7 +63,7 @@ add_openmw_dir (mwlua context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings coremwscriptbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings dialoguebindings postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker landbindings magicbindings factionbindings - classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings + classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings weatherbindings types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 0f82e953c1..0ea8451774 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -433,6 +433,8 @@ OMW::Engine::~Engine() } SDL_Quit(); + + Log(Debug::Info) << "Quitting peacefully."; } // Set data dir @@ -729,7 +731,7 @@ void OMW::Engine::prepareEngine() mVFS = std::make_unique(); - VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); + VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true, &mEncoder.get()->getStatelessEncoder()); mResourceSystem = std::make_unique( mVFS.get(), Settings::cells().mCacheExpiryDelay, &mEncoder.get()->getStatelessEncoder()); @@ -753,7 +755,7 @@ void OMW::Engine::prepareEngine() mViewer->addEventHandler(mScreenCaptureHandler); - mL10nManager = std::make_unique(mVFS.get()); + mL10nManager = std::make_unique(mVFS.get()); mL10nManager->setPreferredLocales(Settings::general().mPreferredLocales, Settings::general().mGmstOverridesL10n); mEnvironment.setL10nManager(*mL10nManager); @@ -779,7 +781,6 @@ void OMW::Engine::prepareEngine() const auto userdefault = mCfgMgr.getUserConfigPath() / "gamecontrollerdb.txt"; const auto localdefault = mCfgMgr.getLocalPath() / "gamecontrollerdb.txt"; - const auto globaldefault = mCfgMgr.getGlobalPath() / "gamecontrollerdb.txt"; std::filesystem::path userGameControllerdb; if (std::filesystem::exists(userdefault)) @@ -788,9 +789,13 @@ void OMW::Engine::prepareEngine() std::filesystem::path gameControllerdb; if (std::filesystem::exists(localdefault)) gameControllerdb = localdefault; - else if (std::filesystem::exists(globaldefault)) - gameControllerdb = globaldefault; - // else if it doesn't exist, pass in an empty string + else if (!mCfgMgr.getGlobalPath().empty()) + { + const auto globaldefault = mCfgMgr.getGlobalPath() / "gamecontrollerdb.txt"; + if (std::filesystem::exists(globaldefault)) + gameControllerdb = globaldefault; + } + // else if it doesn't exist, pass in an empty path // gui needs our shaders path before everything else mResourceSystem->getSceneManager()->setShaderPath(mResDir / "shaders"); @@ -1069,8 +1074,6 @@ void OMW::Engine::go() Settings::Manager::saveUser(mCfgMgr.getUserConfigPath() / "settings.cfg"); Settings::ShaderManager::get().save(); mLuaManager->savePermanentStorage(mCfgMgr.getUserConfigPath()); - - Log(Debug::Info) << "Quitting peacefully."; } void OMW::Engine::setCompileAll(bool all) diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 38e95ea7c8..97b6a78ee9 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -113,7 +113,7 @@ namespace MWDialogue class Journal; } -namespace l10n +namespace L10n { class Manager; } @@ -141,7 +141,7 @@ namespace OMW std::unique_ptr mStateManager; std::unique_ptr mLuaManager; std::unique_ptr mLuaWorker; - std::unique_ptr mL10nManager; + std::unique_ptr mL10nManager; MWBase::Environment mEnvironment; ToUTF8::FromType mEncoding; std::unique_ptr mEncoder; diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index aa8a41b7c1..94f918a60b 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -10,7 +10,7 @@ namespace Resource class ResourceSystem; } -namespace l10n +namespace L10n { class Manager; } @@ -57,7 +57,7 @@ namespace MWBase StateManager* mStateManager = nullptr; LuaManager* mLuaManager = nullptr; Resource::ResourceSystem* mResourceSystem = nullptr; - l10n::Manager* mL10nManager = nullptr; + L10n::Manager* mL10nManager = nullptr; float mFrameRateLimit = 0; float mFrameDuration = 0; @@ -95,7 +95,7 @@ namespace MWBase void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; } - void setL10nManager(l10n::Manager& value) { mL10nManager = &value; } + void setL10nManager(L10n::Manager& value) { mL10nManager = &value; } Misc::NotNullPtr getWorld() const { return mWorld; } Misc::NotNullPtr getWorldModel() const { return mWorldModel; } @@ -122,7 +122,7 @@ namespace MWBase Misc::NotNullPtr getResourceSystem() const { return mResourceSystem; } - Misc::NotNullPtr getL10nManager() const { return mL10nManager; } + Misc::NotNullPtr getL10nManager() const { return mL10nManager; } float getFrameRateLimit() const { return mFrameRateLimit; } diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 5ee20476b3..de6cf91f4e 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -88,6 +88,8 @@ namespace MWBase virtual void executeAction(int action) = 0; virtual bool controlsDisabled() = 0; + + virtual void saveBindings() = 0; }; } diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index a5d6fe1114..5772c555a3 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 #include #include @@ -8,6 +9,7 @@ #include #include "../mwgui/mode.hpp" +#include "../mwmechanics/damagesourcetype.hpp" #include "../mwrender/animationpriority.hpp" #include @@ -38,6 +40,11 @@ namespace LuaUtil } } +namespace osg +{ + class Vec3f; +} + namespace MWBase { // \brief LuaManager is the central interface through which the engine invokes lua scripts. @@ -68,13 +75,19 @@ namespace MWBase const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, uint32_t loops, bool loopfallback) = 0; + virtual void jailTimeServed(const MWWorld::Ptr& actor, int days) = 0; virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0; virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0; + virtual void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType) + = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; // `arg` is either forwarded from MWGui::pushGuiMode or empty virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; + virtual void savePermanentStorage(const std::filesystem::path& userConfigPath) = 0; // TODO: notify LuaManager about other events // virtual void objectOnHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 23d79c1a6b..551d86a041 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -8,9 +8,6 @@ #include #include -#include "../mwmechanics/greetingstate.hpp" -#include "../mwrender/animationpriority.hpp" - #include "../mwworld/ptr.hpp" namespace osg @@ -27,6 +24,11 @@ namespace ESM class ESMWriter; } +namespace MWMechanics +{ + enum class GreetingState; +} + namespace MWWorld { class Ptr; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 8164501b4b..037f719e6d 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -384,6 +384,7 @@ namespace MWBase // Used in Lua bindings virtual const std::vector& getGuiModeStack() const = 0; virtual void setDisabledByLua(std::string_view windowId, bool disabled) = 0; + virtual bool isWindowVisible(std::string_view windowId) const = 0; virtual std::vector getAllWindowIds() const = 0; virtual std::vector getAllowedWindowIds(MWGui::GuiMode mode) const = 0; }; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f268ed0e52..157c12af23 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -22,6 +22,7 @@ namespace osg { class Vec3f; + class Vec4f; class Matrixf; class Quat; class Image; @@ -93,6 +94,7 @@ namespace MWWorld class RefData; class Cell; class DateTimeManager; + class Weather; typedef std::vector> PtrMovementList; } @@ -216,9 +218,21 @@ namespace MWBase virtual void changeWeather(const ESM::RefId& region, const unsigned int id) = 0; - virtual int getCurrentWeather() const = 0; + virtual void changeWeather(const ESM::RefId& region, const ESM::RefId& id) = 0; - virtual int getNextWeather() const = 0; + virtual const std::vector& getAllWeather() const = 0; + + virtual int getCurrentWeatherScriptId() const = 0; + + virtual const MWWorld::Weather& getCurrentWeather() const = 0; + + virtual const MWWorld::Weather* getWeather(size_t index) const = 0; + + virtual const MWWorld::Weather* getWeather(const ESM::RefId& id) const = 0; + + virtual int getNextWeatherScriptId() const = 0; + + virtual const MWWorld::Weather* getNextWeather() const = 0; virtual float getWeatherTransition() const = 0; @@ -478,6 +492,7 @@ namespace MWBase // Allow NPCs to use torches? virtual bool useTorches() const = 0; + virtual const osg::Vec4f& getSunLightPosition() const = 0; virtual float getSunVisibility() const = 0; virtual float getSunPercentage() const = 0; @@ -511,9 +526,6 @@ namespace MWBase /// Spawn a random creature from a levelled list next to the player virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0; - /// Spawn a blood effect for \a ptr at \a worldPosition - virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; - virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) = 0; @@ -574,8 +586,7 @@ 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, - std::span ignore, std::vector* occupyingActors = nullptr) const = 0; + virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 8bf9071f0c..37b0b85d45 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -12,6 +12,8 @@ #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/containerstore.hpp" @@ -109,8 +111,23 @@ namespace MWClass return std::make_pair(slots_, false); } - ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { + // We don't actually need an actor as such. We just need an object that has + // lua scripts and the Combat interface. + if (useLuaInterfaceIfAvailable) + { + // In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine. + MWWorld::Ptr mutablePtr( + const_cast(ptr.mRef), const_cast(ptr.mCell)); + auto res = MWLua::LocalScripts::callPlayerInterface( + "Combat", "getArmorSkill", MWLua::LObject(mutablePtr)); + if (res) + return ESM::RefId::deserializeText(res.value()); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::LiveCellRef* ref = ptr.get(); std::string_view typeGmst; @@ -175,7 +192,7 @@ namespace MWClass const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const { - const ESM::RefId es = getEquipmentSkill(ptr); + const ESM::RefId es = getEquipmentSkill(ptr, false); static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up"); static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up"); static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up"); @@ -190,7 +207,7 @@ namespace MWClass const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const { - const ESM::RefId es = getEquipmentSkill(ptr); + const ESM::RefId es = getEquipmentSkill(ptr, false); static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down"); static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down"); static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down"); @@ -221,24 +238,29 @@ namespace MWClass std::string text; // get armor type string (light/medium/heavy) - std::string_view typeText; + std::string typeText; if (ref->mBase->mData.mWeight == 0) { // no type } else { - const ESM::RefId armorType = getEquipmentSkill(ptr); + const ESM::RefId armorType = getEquipmentSkill(ptr, true); if (armorType == ESM::Skill::LightArmor) typeText = "#{sLight}"; else if (armorType == ESM::Skill::MediumArmor) typeText = "#{sMedium}"; - else + else if (armorType == ESM::Skill::HeavyArmor) typeText = "#{sHeavy}"; + // For other skills, just subtitute the skill name + // Normally you would never see this case, but modding allows getEquipmentSkill() to return any skill. + else + typeText = "#{sSkill" + armorType.toString() + "}"; } text += "\n#{sArmorRating}: " - + MWGui::ToolTips::toString(static_cast(getEffectiveArmorRating(ptr, MWMechanics::getPlayer()))); + + MWGui::ToolTips::toString( + static_cast(getSkillAdjustedArmorRating(ptr, MWMechanics::getPlayer(), true))); int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" @@ -289,11 +311,25 @@ namespace MWClass return record->mId; } - float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor) const + float Armor::getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const { + if (useLuaInterfaceIfAvailable && actor == MWMechanics::getPlayer()) + { + // In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine. + MWWorld::Ptr mutablePtr( + const_cast(ptr.mRef), const_cast(ptr.mCell)); + auto res = MWLua::LocalScripts::callPlayerInterface( + "Combat", "getSkillAdjustedArmorRating", MWLua::LObject(mutablePtr), MWLua::LObject(actor)); + if (res) + return res.value(); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::LiveCellRef* ref = ptr.get(); - const ESM::RefId armorSkillType = getEquipmentSkill(ptr); + const ESM::RefId armorSkillType = getEquipmentSkill(ptr, useLuaInterfaceIfAvailable); float armorSkill = actor.getClass().getSkill(actor, armorSkillType); int iBaseArmorSkill = MWBase::Environment::get() diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index 808bc078f4..e68dfe227f 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -41,7 +41,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. @@ -81,7 +81,8 @@ namespace MWClass bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override; + float getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const override; }; } diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 87d34c56d6..e303635309 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -98,7 +98,7 @@ namespace MWClass return std::make_pair(slots_, false); } - ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { const MWWorld::LiveCellRef* ref = ptr.get(); diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index f95559f9c0..63764695b3 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -33,7 +33,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override; ///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip. diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index c8b1f05972..fff191c22d 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -192,12 +192,13 @@ namespace MWClass { if (!isTrapped) { - if (canBeHarvested(ptr)) - { - return std::make_unique(ptr); - } + if (!canBeHarvested(ptr)) + return std::make_unique(ptr); - return std::make_unique(ptr); + if (hasToolTip(ptr)) + return std::make_unique(ptr); + + return std::make_unique(std::string_view{}, ptr); } else { diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 9224f6f0d8..93052567fa 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -25,11 +25,14 @@ #include "../mwmechanics/setbaseaisetting.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontalk.hpp" #include "../mwworld/cellstore.hpp" @@ -283,8 +286,8 @@ namespace MWClass if (!success) { - victim.getClass().onHit( - victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + 0.0f, false, hitPosition, false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -342,12 +345,12 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit( - victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Creature::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void Creature::onHit(const MWWorld::Ptr& ptr, const std::map& damages, + const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); @@ -360,16 +363,12 @@ namespace MWClass { stats.setAttacked(true); - // No retaliation for totally static creatures (they have no movement or attacks anyway) - if (isMobile(ptr)) - { - bool complain = sourceType == MWMechanics::DamageSourceType::Melee; - bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; - if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) - setOnPcHitMe = false; - else - setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); - } + bool complain = sourceType == MWMechanics::DamageSourceType::Melee; + bool supportFriendlyFire = sourceType != MWMechanics::DamageSourceType::Ranged; + if (supportFriendlyFire && MWMechanics::friendlyHit(attacker, ptr, complain)) + setOnPcHitMe = false; + else + setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } // Attacker and target store each other as hitattemptactor if they have no one stored yet @@ -401,19 +400,44 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - MWBase::Environment::get().getSoundManager()->playSound3D( - ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - if (damage < 0.001f) - damage = 0; + bool hasDamage = false; + bool hasHealthDamage = false; + float healthDamage = 0.f; + for (auto& [stat, damage] : damages) + { + if (damage < 0.001f) + continue; + hasDamage = true; - if (damage > 0.f) + if (stat == "health") + { + hasHealthDamage = true; + healthDamage = damage; + MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); + health.setCurrent(health.getCurrent() - damage); + stats.setHealth(health); + } + else if (stat == "fatigue") + { + MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + fatigue.setCurrent(fatigue.getCurrent() - damage, true); + stats.setFatigue(fatigue); + } + else if (stat == "magicka") + { + MWMechanics::DynamicStat magicka(getCreatureStats(ptr).getMagicka()); + magicka.setCurrent(magicka.getCurrent() - damage); + stats.setMagicka(magicka); + } + } + + if (hasDamage) { if (!attacker.isEmpty()) { @@ -424,35 +448,11 @@ namespace MWClass * getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f + getGmst().iKnockDownOddsBase->mValue.getInteger(); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) + if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? } - - if (ishealth) - { - damage *= damage / (damage + getArmorRating(ptr)); - damage = std::max(1.f, damage); - if (!attacker.isEmpty()) - { - damage = scaleDamage(damage, attacker, ptr); - MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); - } - - MWBase::Environment::get().getSoundManager()->playSound3D( - ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); - - MWMechanics::DynamicStat health(stats.getHealth()); - health.setCurrent(health.getCurrent() - damage); - stats.setHealth(health); - } - else - { - MWMechanics::DynamicStat fatigue(stats.getFatigue()); - fatigue.setCurrent(fatigue.getCurrent() - damage, true); - stats.setFatigue(fatigue); - } } } @@ -534,10 +534,11 @@ namespace MWClass const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); + const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); float moveSpeed; - if (getEncumbrance(ptr) > getCapacity(ptr)) + if (normalizedEncumbrance > 1.0f) moveSpeed = 0.0f; else if (canFly(ptr) || (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) @@ -547,7 +548,6 @@ namespace MWClass + mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude()); flySpeed = gmst.fMinFlySpeed->mValue.getFloat() + flySpeed * (gmst.fMaxFlySpeed->mValue.getFloat() - gmst.fMinFlySpeed->mValue.getFloat()); - const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); flySpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; @@ -595,7 +595,7 @@ namespace MWClass return info; } - float Creature::getArmorRating(const MWWorld::Ptr& ptr) const + float Creature::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { // Equipment armor rating is deliberately ignored. return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); @@ -768,11 +768,6 @@ namespace MWClass } } - int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - return ptr.get()->mBase->mBloodType; - } - void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) @@ -871,7 +866,8 @@ namespace MWClass ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->moveObject( + ptr, ptr.getCell()->getOriginCell(ptr), ptr.getCellRef().getPosition().asVec3()); MWBase::Environment::get().getWorld()->rotateObject( ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index b8619128c2..d7bb63011d 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -66,8 +66,8 @@ namespace MWClass void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, const osg::Vec3f& hitPosition, bool success) const override; - void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; @@ -88,7 +88,7 @@ namespace MWClass ///< Return total weight that fits into the object. Throws an exception, if the object can't /// hold other objects. - float getArmorRating(const MWWorld::Ptr& ptr) const override; + float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override; ///< @return combined armor rating of this actor bool isEssential(const MWWorld::ConstPtr& ptr) const override; @@ -118,9 +118,6 @@ namespace MWClass float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp index f13d6007cd..0e7888317e 100644 --- a/apps/openmw/mwclass/esm4base.hpp +++ b/apps/openmw/mwclass/esm4base.hpp @@ -55,6 +55,16 @@ namespace MWClass } return res; } + + // TODO: Figure out a better way to find markers and LOD meshes + inline bool isMarkerModel(std::string_view model) + { + return Misc::StringUtils::ciStartsWith(model, "marker"); + } + inline bool isLodModel(std::string_view model) + { + return Misc::StringUtils::ciEndsWith(model, "lod.nif"); + } } // Base for many ESM4 Classes @@ -100,11 +110,8 @@ namespace MWClass { std::string_view model = getClassModel(ptr); - // Hide meshes meshes/marker/* and *LOD.nif in ESM4 cells. It is a temporarty hack. - // Needed because otherwise LOD meshes are rendered on top of normal meshes. - // TODO: Figure out a better way find markers and LOD meshes; show LOD only outside of active grid. - if (model.empty() || Misc::StringUtils::ciStartsWith(model, "marker") - || Misc::StringUtils::ciEndsWith(model, "lod.nif")) + // TODO: There should be a better way to hide markers + if (ESM4Impl::isMarkerModel(model) || ESM4Impl::isLodModel(model)) return {}; return model; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 0b61436d11..da0e78bcd3 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -29,6 +29,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/localscripts.hpp" + #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aisetting.hpp" #include "../mwmechanics/autocalcspell.hpp" @@ -620,8 +622,8 @@ namespace MWClass float damage = 0.0f; if (!success) { - othercls.onHit( - victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, false, hitPosition, false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); return; @@ -694,14 +696,13 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee); + MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength, + damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee); } - void Npc::onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, - const MWMechanics::DamageSourceType sourceType) const + void Npc::onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { - MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); @@ -748,23 +749,47 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); - if (damage < 0.001f) - damage = 0; + if (ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) + return; - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + bool hasDamage = false; + bool hasHealthDamage = false; + float healthDamage = 0.f; + for (auto& [stat, damage] : damages) + { + if (damage < 0.001f) + continue; + hasDamage = true; - if (godmode) - damage = 0; + if (stat == "health") + { + hasHealthDamage = true; + healthDamage = damage; + MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); + health.setCurrent(health.getCurrent() - damage); + stats.setHealth(health); + } + else if (stat == "fatigue") + { + MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + fatigue.setCurrent(fatigue.getCurrent() - damage, true); + stats.setFatigue(fatigue); + } + else if (stat == "magicka") + { + MWMechanics::DynamicStat magicka(getCreatureStats(ptr).getMagicka()); + magicka.setCurrent(magicka.getCurrent() - damage); + stats.setMagicka(magicka); + } + } - if (damage > 0.0f && !attacker.isEmpty()) + if (hasDamage && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. @@ -783,109 +808,16 @@ namespace MWClass float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); - if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) + if (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? - - if (damage > 0 && ishealth) - { - // Hit percentages: - // cuirass = 30% - // shield, helmet, greaves, boots, pauldrons = 10% each - // guantlets = 5% each - static const int hitslots[20] - = { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, - MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, - MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, - MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, - MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, - MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, - MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, - MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; - int hitslot = hitslots[Misc::Rng::rollDice(20, prng)]; - - float unmitigatedDamage = damage; - float x = damage / (damage + getArmorRating(ptr)); - damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x); - int damageDiff = static_cast(unmitigatedDamage - damage); - damage = std::max(1.f, damage); - damageDiff = std::max(1, damageDiff); - - MWWorld::InventoryStore& inv = getInventoryStore(ptr); - MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); - MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); - bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; - // If there's no item in the carried left slot or if it is not a shield redistribute the hit. - if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) - { - if (Misc::Rng::rollDice(2, prng) == 0) - hitslot = MWWorld::InventoryStore::Slot_Cuirass; - else - hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; - armorslot = inv.getSlot(hitslot); - if (armorslot != inv.end()) - { - armor = *armorslot; - hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; - } - } - if (hasArmor) - { - // Unarmed creature attacks don't affect armor condition unless it was - // explicitly requested. - if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc() - || Settings::game().mUnarmedCreatureAttacksDamageArmor) - { - int armorhealth = armor.getClass().getItemHealth(armor); - armorhealth -= std::min(damageDiff, armorhealth); - armor.getCellRef().setCharge(armorhealth); - - // Armor broken? unequip it - if (armorhealth == 0) - armor = *inv.unequipItem(armor); - } - - ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); - if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); - - if (skill == ESM::Skill::LightArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::MediumArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); - else if (skill == ESM::Skill::HeavyArmor) - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); - } - else if (ptr == MWMechanics::getPlayer()) - skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); - } } - if (ishealth) + if (hasHealthDamage && healthDamage > 0.0f) { - if (!attacker.isEmpty() && !godmode) - damage = scaleDamage(damage, attacker, ptr); - - if (damage > 0.0f) - { - sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); - if (ptr == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->activateHitOverlay(); - if (!attacker.isEmpty()) - MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); - } - MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); - health.setCurrent(health.getCurrent() - damage); - stats.setHealth(health); - } - else - { - MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); - fatigue.setCurrent(fatigue.getCurrent() - damage, true); - stats.setFatigue(fatigue); + if (ptr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->activateHitOverlay(); } if (!wasDead && getCreatureStats(ptr).isDead()) @@ -990,15 +922,10 @@ namespace MWClass const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); - - bool swimming = world->isSwimming(ptr); - bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); - bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run); - bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); - running = running && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); + const bool running = MWBase::Environment::get().getMechanicsManager()->isRunning(ptr); float moveSpeed; - if (getEncumbrance(ptr) > getCapacity(ptr)) + if (normalizedEncumbrance > 1.0f) moveSpeed = 0.0f; else if (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { @@ -1011,9 +938,9 @@ namespace MWClass flySpeed = std::max(0.0f, flySpeed); moveSpeed = flySpeed; } - else if (swimming) + else if (world->isSwimming(ptr)) moveSpeed = getSwimSpeed(ptr); - else if (running && !sneaking) + else if (running && !MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr)) moveSpeed = getRunSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); @@ -1141,8 +1068,17 @@ namespace MWClass MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } - float Npc::getArmorRating(const MWWorld::Ptr& ptr) const + float Npc::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { + if (useLuaInterfaceIfAvailable && ptr == MWMechanics::getPlayer()) + { + auto res = MWLua::LocalScripts::callPlayerInterface("Combat", "getArmorRating"); + if (res) + return res.value(); + } + + // Fallback to the old engine implementation when actors don't have their scripts attached yet. + const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); @@ -1164,7 +1100,7 @@ namespace MWClass } else { - ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); + ratings[i] = it->getClass().getSkillAdjustedArmorRating(*it, ptr); // Take in account armor condition const bool hasHealth = it->getClass().hasItemHealth(*it); @@ -1313,11 +1249,6 @@ namespace MWClass return getNpcStats(ptr).getSkill(id).getModified(); } - int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - return ptr.get()->mBase->mBloodType; - } - void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) @@ -1427,7 +1358,8 @@ namespace MWClass ptr.getRefData().setCustomData(nullptr); // Reset to original position - MWBase::Environment::get().getWorld()->moveObject(ptr, ptr.getCellRef().getPosition().asVec3()); + MWBase::Environment::get().getWorld()->moveObject( + ptr, ptr.getCell()->getOriginCell(ptr), ptr.getCellRef().getPosition().asVec3()); MWBase::Environment::get().getWorld()->rotateObject( ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } @@ -1508,14 +1440,8 @@ namespace MWClass float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { - const MWBase::World* world = MWBase::Environment::get().getWorld(); - const MWMechanics::NpcStats& stats = getNpcStats(ptr); - const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); - const bool swimming = world->isSwimming(ptr); - const bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); - const bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run) - && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); - - return getSwimSpeedImpl(ptr, getGmst(), mageffects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); + const MWMechanics::MagicEffects& effects = getNpcStats(ptr).getMagicEffects(); + const bool running = MWBase::Environment::get().getMechanicsManager()->isRunning(ptr); + return getSwimSpeedImpl(ptr, getGmst(), effects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); } } diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 29ab459242..b038d47337 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -81,8 +81,8 @@ namespace MWClass void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim, const osg::Vec3f& hitPosition, bool success) const override; - void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + void onHit(const MWWorld::Ptr& ptr, const std::map& damages, const MWWorld::Ptr& object, + const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const override; void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const override; @@ -112,7 +112,7 @@ namespace MWClass ///< Returns total weight of objects inside this object (including modifications from magic /// effects). Throws an exception, if the object can't hold other objects. - float getArmorRating(const MWWorld::Ptr& ptr) const override; + float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override; ///< @return combined armor rating of this actor void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override; @@ -137,9 +137,6 @@ namespace MWClass float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; - bool isNpc() const override { return true; } void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 089c8c2894..bee68a52e5 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -105,7 +105,7 @@ namespace MWClass return std::make_pair(slots_, stack); } - ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const + ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; @@ -270,14 +270,14 @@ namespace MWClass std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { - if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) - return { 0, "#{sInventoryMessage1}" }; - // Do not allow equip weapons from inventory during attack if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) return { 0, "#{sCantEquipWeapWarning}" }; + if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) + return { 0, "#{sInventoryMessage1}" }; + std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index 9e79532bc0..96a2a0aa47 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -42,7 +42,7 @@ namespace MWClass ///< \return first: Return IDs of the slot this object can be equipped in; second: can object /// stay stacked when equipped? - ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override; + ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override; int getValue(const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 9f3bb0b26f..ab92300804 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -448,12 +449,14 @@ namespace MWDialogue { updateOriginalDisposition(); MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); - // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with - // intimidate) - npcStats.setBaseDisposition(0); - int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); - int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); + // Get the sum of disposition effects minus charm (shouldn't be made permanent) + npcStats.setBaseDisposition(0); + int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false) + - npcStats.getMagicEffects().getOrDefault(ESM::MagicEffect::Charm).getMagnitude(); + + // Clamp new permanent disposition to avoid negative derived disposition (can be caused by intimidate) + int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); npcStats.setBaseDisposition(disposition); } mPermanentDispositionChange = 0; diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index b7d4a1361c..a5a3be85f9 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -501,7 +501,7 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons case ESM::DialogueCondition::Function_Weather: - return MWBase::Environment::get().getWorld()->getCurrentWeather(); + return MWBase::Environment::get().getWorld()->getCurrentWeatherScriptId(); case ESM::DialogueCondition::Function_Reputation: if (!mActor.getClass().isNpc()) diff --git a/apps/openmw/mwgui/companionwindow.cpp b/apps/openmw/mwgui/companionwindow.cpp index 240198eddc..52fc4cc4ce 100644 --- a/apps/openmw/mwgui/companionwindow.cpp +++ b/apps/openmw/mwgui/companionwindow.cpp @@ -14,6 +14,7 @@ #include "companionitemmodel.hpp" #include "countdialog.hpp" #include "draganddrop.hpp" +#include "itemtransfer.hpp" #include "itemview.hpp" #include "messagebox.hpp" #include "sortfilteritemmodel.hpp" @@ -38,12 +39,14 @@ namespace namespace MWGui { - CompanionWindow::CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager) + CompanionWindow::CompanionWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer, MessageBoxManager* manager) : WindowBase("openmw_companion_window.layout") , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) - , mDragAndDrop(dragAndDrop) + , mUpdateNextFrame(false) + , mDragAndDrop(&dragAndDrop) + , mItemTransfer(&itemTransfer) , mMessageBoxManager(manager) { getWidget(mCloseButton, "CloseButton"); @@ -93,8 +96,14 @@ namespace MWGui name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); - dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); + + if (MyGUI::InputManager::getInstance().isAltPressed()) + dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::transferItem); + else + dialog->eventOkClicked += MyGUI::newDelegate(this, &CompanionWindow::dragItem); } + else if (MyGUI::InputManager::getInstance().isAltPressed()) + transferItem(nullptr, count); else dragItem(nullptr, count); } @@ -105,11 +114,16 @@ namespace MWGui mItemView->update(); } - void CompanionWindow::dragItem(MyGUI::Widget* sender, int count) + void CompanionWindow::dragItem(MyGUI::Widget* /*sender*/, std::size_t count) { mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } + void CompanionWindow::transferItem(MyGUI::Widget* /*sender*/, std::size_t count) + { + mItemTransfer->apply(mModel->getItem(mSelectedItem), count, *mItemView); + } + void CompanionWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) @@ -134,12 +148,20 @@ namespace MWGui mItemView->resetScrollBars(); setTitle(actor.getClass().getName(actor)); + + mPtr.getClass().getContainerStore(mPtr).setContListener(this); } void CompanionWindow::onFrame(float dt) { checkReferenceAvailable(); - updateEncumbranceBar(); + + if (mUpdateNextFrame) + { + updateEncumbranceBar(); + mItemView->update(); + mUpdateNextFrame = false; + } } void CompanionWindow::updateEncumbranceBar() @@ -202,4 +224,23 @@ namespace MWGui mSortModel = nullptr; } + void CompanionWindow::itemAdded(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + + void CompanionWindow::itemRemoved(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + + void CompanionWindow::onOpen() + { + mItemTransfer->addTarget(*mItemView); + } + + void CompanionWindow::onClose() + { + mItemTransfer->removeTarget(*mItemView); + } } diff --git a/apps/openmw/mwgui/companionwindow.hpp b/apps/openmw/mwgui/companionwindow.hpp index 97f3a0072e..5e78d17334 100644 --- a/apps/openmw/mwgui/companionwindow.hpp +++ b/apps/openmw/mwgui/companionwindow.hpp @@ -4,6 +4,10 @@ #include "referenceinterface.hpp" #include "windowbase.hpp" +#include "../mwworld/containerstore.hpp" + +#include + namespace MWGui { namespace Widgets @@ -16,11 +20,12 @@ namespace MWGui class DragAndDrop; class SortFilterItemModel; class CompanionItemModel; + class ItemTransfer; - class CompanionWindow : public WindowBase, public ReferenceInterface + class CompanionWindow : public WindowBase, public ReferenceInterface, public MWWorld::ContainerStoreListener { public: - CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager); + explicit CompanionWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer, MessageBoxManager* manager); bool exit() override; @@ -30,6 +35,9 @@ namespace MWGui void onFrame(float dt) override; void clear() override { resetReference(); } + void itemAdded(const MWWorld::ConstPtr& item, int count) override; + void itemRemoved(const MWWorld::ConstPtr& item, int count) override; + std::string_view getWindowIdForLua() const override { return "Companion"; } private: @@ -37,8 +45,10 @@ namespace MWGui SortFilterItemModel* mSortModel; CompanionItemModel* mModel; int mSelectedItem; + bool mUpdateNextFrame; - DragAndDrop* mDragAndDrop; + Misc::NotNullPtr mDragAndDrop; + Misc::NotNullPtr mItemTransfer; MyGUI::Button* mCloseButton; MyGUI::EditBox* mFilterEdit; @@ -49,7 +59,8 @@ namespace MWGui void onItemSelected(int index); void onNameFilterChanged(MyGUI::EditBox* _sender); void onBackgroundSelected(); - void dragItem(MyGUI::Widget* sender, int count); + void dragItem(MyGUI::Widget* sender, std::size_t count); + void transferItem(MyGUI::Widget* sender, std::size_t count); void onMessageBoxButtonClicked(int button); @@ -58,6 +69,10 @@ namespace MWGui void onCloseButtonClicked(MyGUI::Widget* _sender); void onReferenceUnavailable() override; + + void onOpen() override; + + void onClose() override; }; } diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index 6ab2c862d4..937bab0851 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -18,12 +18,12 @@ #include "../mwscript/interpretercontext.hpp" -#include "countdialog.hpp" -#include "inventorywindow.hpp" - #include "containeritemmodel.hpp" +#include "countdialog.hpp" #include "draganddrop.hpp" #include "inventoryitemmodel.hpp" +#include "inventorywindow.hpp" +#include "itemtransfer.hpp" #include "itemview.hpp" #include "pickpocketitemmodel.hpp" #include "sortfilteritemmodel.hpp" @@ -32,12 +32,14 @@ namespace MWGui { - ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) + ContainerWindow::ContainerWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer) : WindowBase("openmw_container_window.layout") - , mDragAndDrop(dragAndDrop) + , mDragAndDrop(&dragAndDrop) + , mItemTransfer(&itemTransfer) , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) + , mUpdateNextFrame(false) , mTreatNextOpenAsLoot(false) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); @@ -88,26 +90,47 @@ namespace MWGui name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, "#{sTake}", count); dialog->eventOkClicked.clear(); - dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::dragItem); + + if (MyGUI::InputManager::getInstance().isAltPressed()) + dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::transferItem); + else + dialog->eventOkClicked += MyGUI::newDelegate(this, &ContainerWindow::dragItem); } + else if (MyGUI::InputManager::getInstance().isAltPressed()) + transferItem(nullptr, count); else dragItem(nullptr, count); } - void ContainerWindow::dragItem(MyGUI::Widget* sender, int count) + void ContainerWindow::dragItem(MyGUI::Widget* /*sender*/, std::size_t count) { - if (!mModel) + if (mModel == nullptr) return; - if (!onTakeItem(mModel->getItem(mSelectedItem), count)) + const ItemStack item = mModel->getItem(mSelectedItem); + + if (!mModel->onTakeItem(item.mBase, count)) return; mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } + void ContainerWindow::transferItem(MyGUI::Widget* /*sender*/, std::size_t count) + { + if (mModel == nullptr) + return; + + const ItemStack item = mModel->getItem(mSelectedItem); + + if (!mModel->onTakeItem(item.mBase, count)) + return; + + mItemTransfer->apply(item, count, *mItemView); + } + void ContainerWindow::dropItem() { - if (!mModel) + if (mModel == nullptr) return; bool success = mModel->onDropItem(mDragAndDrop->mItem.mBase, mDragAndDrop->mDraggedCount); @@ -160,6 +183,8 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); setTitle(container.getClass().getName(container)); + + mPtr.getClass().getContainerStore(mPtr).setContListener(this); } void ContainerWindow::resetReference() @@ -170,10 +195,13 @@ namespace MWGui mSortModel = nullptr; } + void ContainerWindow::onOpen() + { + mItemTransfer->addTarget(*mItemView); + } + void ContainerWindow::onClose() { - WindowBase::onClose(); - // Make sure the window was actually closed and not temporarily hidden. if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container)) return; @@ -184,6 +212,8 @@ namespace MWGui if (!mPtr.isEmpty()) MWBase::Environment::get().getMechanicsManager()->onClose(mPtr); resetReference(); + + mItemTransfer->removeTarget(*mItemView); } void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) @@ -231,9 +261,9 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->playSound(sound); } - const ItemStack& item = mModel->getItem(i); + const ItemStack item = mModel->getItem(i); - if (!onTakeItem(item, item.mCount)) + if (!mModel->onTakeItem(item.mBase, item.mCount)) break; mModel->moveItem(item, item.mCount, playerModel); @@ -310,14 +340,30 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } - bool ContainerWindow::onTakeItem(const ItemStack& item, int count) - { - 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); } + + void ContainerWindow::onFrame(float dt) + { + checkReferenceAvailable(); + + if (mUpdateNextFrame) + { + mItemView->update(); + mUpdateNextFrame = false; + } + } + + void ContainerWindow::itemAdded(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + + void ContainerWindow::itemRemoved(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } } diff --git a/apps/openmw/mwgui/container.hpp b/apps/openmw/mwgui/container.hpp index 555fa8e1ae..86ded2ff75 100644 --- a/apps/openmw/mwgui/container.hpp +++ b/apps/openmw/mwgui/container.hpp @@ -1,10 +1,13 @@ #ifndef MGUI_CONTAINER_H #define MGUI_CONTAINER_H +#include "itemmodel.hpp" #include "referenceinterface.hpp" #include "windowbase.hpp" -#include "itemmodel.hpp" +#include + +#include "../mwworld/containerstore.hpp" namespace MyGUI { @@ -17,20 +20,22 @@ namespace MWGui class ContainerWindow; class ItemView; class SortFilterItemModel; -} + class ItemTransfer; -namespace MWGui -{ - class ContainerWindow : public WindowBase, public ReferenceInterface + class ContainerWindow : public WindowBase, public ReferenceInterface, public MWWorld::ContainerStoreListener { public: - ContainerWindow(DragAndDrop* dragAndDrop); + explicit ContainerWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer); void setPtr(const MWWorld::Ptr& container) override; + + void onOpen() override; + void onClose() override; + void clear() override { resetReference(); } - void onFrame(float dt) override { checkReferenceAvailable(); } + void onFrame(float dt) override; void resetReference() override; @@ -38,15 +43,20 @@ namespace MWGui void treatNextOpenAsLoot() { mTreatNextOpenAsLoot = true; } + void itemAdded(const MWWorld::ConstPtr& item, int count) override; + void itemRemoved(const MWWorld::ConstPtr& item, int count) override; + std::string_view getWindowIdForLua() const override { return "Container"; } private: - DragAndDrop* mDragAndDrop; + Misc::NotNullPtr mDragAndDrop; + Misc::NotNullPtr mItemTransfer; MWGui::ItemView* mItemView; SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; + bool mUpdateNextFrame; bool mTreatNextOpenAsLoot; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; @@ -54,15 +64,13 @@ namespace MWGui void onItemSelected(int index); void onBackgroundSelected(); - void dragItem(MyGUI::Widget* sender, int count); + void dragItem(MyGUI::Widget* sender, std::size_t count); + void transferItem(MyGUI::Widget* sender, std::size_t count); void dropItem(); void onCloseButtonClicked(MyGUI::Widget* _sender); void onTakeAllButtonClicked(MyGUI::Widget* _sender); void onDisposeCorpseButtonClicked(MyGUI::Widget* sender); - /// @return is taking the item allowed? - bool onTakeItem(const ItemStack& item, int count); - void onReferenceUnavailable() override; }; } diff --git a/apps/openmw/mwgui/containeritemmodel.cpp b/apps/openmw/mwgui/containeritemmodel.cpp index 09b66672ba..ff50431d86 100644 --- a/apps/openmw/mwgui/containeritemmodel.cpp +++ b/apps/openmw/mwgui/containeritemmodel.cpp @@ -224,7 +224,7 @@ namespace MWGui if (target.getType() != ESM::Container::sRecordId) return true; - // check container organic flag + // Check container organic flag MWWorld::LiveCellRef* ref = target.get(); if (ref->mBase->mFlags & ESM::Container::Organic) { @@ -232,9 +232,18 @@ namespace MWGui return false; } - // check that we don't exceed container capacity - float weight = item.getClass().getWeight(item) * count; - if (target.getClass().getCapacity(target) < target.getClass().getEncumbrance(target) + weight) + // Check for container without capacity + float capacity = target.getClass().getCapacity(target); + if (capacity <= 0.0f) + { + MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); + return false; + } + + // Check the container capacity plus one increment so the expected total weight can + // fit in the container with floating-point imprecision + float newEncumbrance = target.getClass().getEncumbrance(target) + (item.getClass().getWeight(item) * count); + if (std::nextafterf(capacity, std::numeric_limits::max()) < newEncumbrance) { MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); return false; diff --git a/apps/openmw/mwgui/countdialog.hpp b/apps/openmw/mwgui/countdialog.hpp index 9cdf231549..70fc820899 100644 --- a/apps/openmw/mwgui/countdialog.hpp +++ b/apps/openmw/mwgui/countdialog.hpp @@ -16,12 +16,10 @@ namespace MWGui CountDialog(); void openCountDialog(const std::string& item, const std::string& message, const int maxCount); - typedef MyGUI::delegates::MultiDelegate EventHandle_WidgetInt; - /** Event : Ok button was clicked.\n - signature : void method(MyGUI::Widget* _sender, int _count)\n + signature : void method(MyGUI::Widget* sender, std::size_t count)\n */ - EventHandle_WidgetInt eventOkClicked; + MyGUI::delegates::MultiDelegate eventOkClicked; private: MyGUI::ScrollBar* mSlider; diff --git a/apps/openmw/mwgui/draganddrop.cpp b/apps/openmw/mwgui/draganddrop.cpp index 0fa2cc4e21..85229d0a13 100644 --- a/apps/openmw/mwgui/draganddrop.cpp +++ b/apps/openmw/mwgui/draganddrop.cpp @@ -11,7 +11,6 @@ #include "controllers.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" -#include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui @@ -28,7 +27,7 @@ namespace MWGui } void DragAndDrop::startDrag( - int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count) + int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, std::size_t count) { mItem = sourceModel->getItem(index); mDraggedCount = count; @@ -72,25 +71,25 @@ namespace MWGui mSourceSortModel->addDragItem(mItem.mBase, count); } - ItemWidget* baseWidget = MyGUI::Gui::getInstance().createWidget( + mDraggedWidget = MyGUI::Gui::getInstance().createWidget( "MW_ItemIcon", 0, 0, 42, 42, MyGUI::Align::Default, "DragAndDrop"); Controllers::ControllerFollowMouse* controller = MyGUI::ControllerManager::getInstance() .createItem(Controllers::ControllerFollowMouse::getClassTypeName()) ->castType(); - MyGUI::ControllerManager::getInstance().addItem(baseWidget, controller); + MyGUI::ControllerManager::getInstance().addItem(mDraggedWidget, controller); - mDraggedWidget = baseWidget; - baseWidget->setItem(mItem.mBase); - baseWidget->setNeedMouseFocus(false); - baseWidget->setCount(count); - - sourceView->update(); + mDraggedWidget->setItem(mItem.mBase); + mDraggedWidget->setNeedMouseFocus(false); + mDraggedWidget->setCount(count); MWBase::Environment::get().getWindowManager()->setDragDrop(true); mIsOnDragAndDrop = true; + + // Update item view after completing drag-and-drop setup + mSourceView->update(); } void DragAndDrop::drop(ItemModel* targetModel, ItemView* targetView) @@ -124,6 +123,22 @@ namespace MWGui mSourceView->update(); } + void DragAndDrop::update() + { + if (!mIsOnDragAndDrop) + return; + + const unsigned count = mItem.mBase.getCellRef().getAbsCount(); + if (count >= mDraggedCount) + return; + + mItem.mCount = count; + mDraggedCount = count; + mDraggedWidget->setCount(mDraggedCount); + mSourceSortModel->clearDragItems(); + mSourceSortModel->addDragItem(mItem.mBase, mDraggedCount); + } + void DragAndDrop::onFrame() { if (mIsOnDragAndDrop && mItem.mBase.getCellRef().getCount() == 0) @@ -137,8 +152,12 @@ namespace MWGui // since mSourceView doesn't get updated in drag() MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); - MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); - mDraggedWidget = nullptr; + if (mDraggedWidget) + { + MyGUI::Gui::getInstance().destroyWidget(mDraggedWidget); + mDraggedWidget = nullptr; + } + MWBase::Environment::get().getWindowManager()->setDragDrop(false); } diff --git a/apps/openmw/mwgui/draganddrop.hpp b/apps/openmw/mwgui/draganddrop.hpp index fab7f30d75..511e2b7765 100644 --- a/apps/openmw/mwgui/draganddrop.hpp +++ b/apps/openmw/mwgui/draganddrop.hpp @@ -2,6 +2,9 @@ #define OPENMW_MWGUI_DRAGANDDROP_H #include "itemmodel.hpp" +#include "itemwidget.hpp" + +#include namespace MyGUI { @@ -18,18 +21,19 @@ namespace MWGui { public: bool mIsOnDragAndDrop; - MyGUI::Widget* mDraggedWidget; + ItemWidget* mDraggedWidget; ItemModel* mSourceModel; ItemView* mSourceView; SortFilterItemModel* mSourceSortModel; ItemStack mItem; - int mDraggedCount; + std::size_t mDraggedCount; DragAndDrop(); void startDrag( - int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); + int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, std::size_t count); void drop(ItemModel* targetModel, ItemView* targetView); + void update(); void onFrame(); void finish(); diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 0a37c93b4f..e5adce7624 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -25,67 +25,12 @@ #include "draganddrop.hpp" #include "inventorywindow.hpp" -#include "itemmodel.hpp" -#include "spellicons.hpp" - #include "itemwidget.hpp" +#include "spellicons.hpp" +#include "worlditemmodel.hpp" namespace MWGui { - - /** - * Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. - */ - class WorldItemModel : public ItemModel - { - public: - WorldItemModel(float left, float top) - : mLeft(left) - , mTop(top) - { - } - virtual ~WorldItemModel() override {} - - MWWorld::Ptr dropItemImpl(const ItemStack& item, size_t count, bool copy) - { - MWBase::World* world = MWBase::Environment::get().getWorld(); - - MWWorld::Ptr dropped; - if (world->canPlaceObject(mLeft, mTop)) - dropped = world->placeObject(item.mBase, mLeft, mTop, count, copy); - else - dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count, copy); - dropped.getCellRef().setOwner(ESM::RefId()); - - return dropped; - } - - MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override - { - return dropItemImpl(item, count, false); - } - - MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override - { - return dropItemImpl(item, count, true); - } - - void removeItem(const ItemStack& item, size_t count) override - { - throw std::runtime_error("removeItem 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 - float mLeft; - float mTop; - }; - HUD::HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) : WindowBase("openmw_hud.layout") , LocalMapBase(customMarkers, localMapRender, Settings::map().mLocalMapHudFogOfWar) @@ -251,16 +196,14 @@ namespace MWGui MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); if (mDragAndDrop->mIsOnDragAndDrop) { + const MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + const MyGUI::IntPoint cursorPosition = MyGUI::InputManager::getInstance().getMousePosition(); + const float cursorX = cursorPosition.left / static_cast(viewSize.width); + const float cursorY = cursorPosition.top / static_cast(viewSize.height); + // drop item into the gameworld - MWBase::Environment::get().getWorld()->breakInvisibility(MWMechanics::getPlayer()); - - MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - MyGUI::IntPoint cursorPosition = MyGUI::InputManager::getInstance().getMousePosition(); - float mouseX = cursorPosition.left / float(viewSize.width); - float mouseY = cursorPosition.top / float(viewSize.height); - - WorldItemModel drop(mouseX, mouseY); - mDragAndDrop->drop(&drop, nullptr); + WorldItemModel worldItemModel(cursorX, cursorY); + mDragAndDrop->drop(&worldItemModel, nullptr); winMgr->changePointer("arrow"); } diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index b4a2024052..da30fa86ff 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -34,6 +34,7 @@ #include "countdialog.hpp" #include "draganddrop.hpp" #include "inventoryitemmodel.hpp" +#include "itemtransfer.hpp" #include "itemview.hpp" #include "settings.hpp" #include "sortfilteritemmodel.hpp" @@ -74,10 +75,11 @@ namespace MWGui } } - InventoryWindow::InventoryWindow( - DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem) + InventoryWindow::InventoryWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer, osg::Group* parent, + Resource::ResourceSystem* resourceSystem) : WindowPinnableBase("openmw_inventory_window.layout") - , mDragAndDrop(dragAndDrop) + , mDragAndDrop(&dragAndDrop) + , mItemTransfer(&itemTransfer) , mSelectedItem(-1) , mSortModel(nullptr) , mTradeModel(nullptr) @@ -86,10 +88,10 @@ namespace MWGui , mLastYSize(0) , mPreview(std::make_unique(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) - , mUpdateTimer(0.f) + , mUpdateNextFrame(false) { mPreviewTexture - = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); + = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord @@ -145,6 +147,8 @@ namespace MWGui auto tradeModel = std::make_unique(std::make_unique(mPtr), MWWorld::Ptr()); mTradeModel = tradeModel.get(); + mPtr.getClass().getInventoryStore(mPtr).setContListener(this); + if (mSortModel) // reuse existing SortModel when possible to keep previous category/filter settings mSortModel->setSourceModel(std::move(tradeModel)); else @@ -310,17 +314,24 @@ namespace MWGui name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); + if (mTrading) dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::sellItem); + else if (MyGUI::InputManager::getInstance().isAltPressed()) + dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::transferItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::dragItem); + mSelectedItem = index; } else { mSelectedItem = index; + if (mTrading) sellItem(nullptr, count); + else if (MyGUI::InputManager::getInstance().isAltPressed()) + transferItem(nullptr, count); else dragItem(nullptr, count); } @@ -359,14 +370,21 @@ namespace MWGui } } - void InventoryWindow::dragItem(MyGUI::Widget* sender, int count) + void InventoryWindow::dragItem(MyGUI::Widget* /*sender*/, std::size_t count) { ensureSelectedItemUnequipped(count); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mTradeModel, mItemView, count); notifyContentChanged(); } - void InventoryWindow::sellItem(MyGUI::Widget* sender, int count) + void InventoryWindow::transferItem(MyGUI::Widget* /*sender*/, std::size_t count) + { + ensureSelectedItemUnequipped(count); + mItemTransfer->apply(mTradeModel->getItem(mSelectedItem), count, *mItemView); + notifyContentChanged(); + } + + void InventoryWindow::sellItem(MyGUI::Widget* /*sender*/, std::size_t count) { ensureSelectedItemUnequipped(count); const ItemStack& item = mTradeModel->getItem(mSelectedItem); @@ -413,6 +431,13 @@ namespace MWGui notifyContentChanged(); } adjustPanes(); + + mItemTransfer->addTarget(*mItemView); + } + + void InventoryWindow::onClose() + { + mItemTransfer->removeTarget(*mItemView); } void InventoryWindow::onWindowResize(MyGUI::Window* _sender) @@ -445,11 +470,10 @@ namespace MWGui if (mPtr.isEmpty()) return; - mArmorRating->setCaptionWithReplacing( - "#{sArmor}: " + MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + auto rating = MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr, true))); + mArmorRating->setCaptionWithReplacing("#{sArmor}: " + rating); if (mArmorRating->getTextSize().width > mArmorRating->getSize().width) - mArmorRating->setCaptionWithReplacing( - MyGUI::utility::toString(static_cast(mPtr.getClass().getArmorRating(mPtr)))); + mArmorRating->setCaptionWithReplacing(rating); } void InventoryWindow::updatePreviewSize() @@ -520,36 +544,25 @@ namespace MWGui } MWWorld::Ptr player = MWMechanics::getPlayer(); + auto type = ptr.getType(); + bool isWeaponOrArmor = type == ESM::Weapon::sRecordId || type == ESM::Armor::sRecordId; + bool isBroken = ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0; - // early-out for items that need to be equipped, but can't be equipped: we don't want to set OnPcEquip in that - // case - if (!ptr.getClass().getEquipmentSlots(ptr).first.empty()) + // In vanilla, broken armor or weapons cannot be equipped + // tools with 0 charges is equippable + if (isBroken && isWeaponOrArmor) { - if (ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); - updateItemView(); - return; - } - - if (!force) - { - auto canEquip = ptr.getClass().canBeEquipped(ptr, player); - - if (canEquip.first == 0) - { - MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second); - updateItemView(); - return; - } - } + MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); + return; } + bool canEquip = ptr.getClass().canBeEquipped(ptr, mPtr).first != 0; + bool shouldSetOnPcEquip = canEquip || force; + // If the item has a script, set OnPCEquip or PCSkipEquip to 1 - if (!script.empty()) + if (!script.empty() && shouldSetOnPcEquip) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here - auto type = ptr.getType(); bool isBook = type == ESM::Book::sRecordId; if (!isBook && type != ESM::Ingredient::sRecordId && type != ESM::Repair::sRecordId) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); @@ -559,24 +572,37 @@ namespace MWGui } std::unique_ptr action = ptr.getClass().use(ptr, force); - action->execute(player); - // Handles partial equipping (final part) - if (mEquippedStackableCount.has_value()) + MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); + auto [eqSlots, canStack] = ptr.getClass().getEquipmentSlots(ptr); + bool isFromDragAndDrop = mDragAndDrop->mItem.mBase == ptr; + int useCount = isFromDragAndDrop ? mDragAndDrop->mDraggedCount : ptr.getCellRef().getCount(); + + if (!eqSlots.empty()) { - // the count to unequip - int count = ptr.getCellRef().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value(); - if (count > 0) - { - MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - invStore.unequipItemQuantity(ptr, count); - updateItemView(); - } - mEquippedStackableCount.reset(); + MWWorld::ContainerStoreIterator it = invStore.getSlot(eqSlots.front()); + if (it != invStore.end() && it->getCellRef().getRefId() == ptr.getCellRef().getRefId()) + useCount += it->getCellRef().getCount(); } + action->execute(player, !canEquip); + + // Partial equipping + int excess = ptr.getCellRef().getCount() - useCount; + if (excess > 0 && canStack) + invStore.unequipItemQuantity(ptr, excess); + if (isVisible()) { + if (isFromDragAndDrop) + { + // Feature: Don't stop draganddrop if potion or ingredient was used + if (ptr.getType() != ESM::Potion::sRecordId && ptr.getType() != ESM::Ingredient::sRecordId) + mDragAndDrop->finish(); + else + mDragAndDrop->update(); + } + mItemView->update(); notifyContentChanged(); @@ -590,7 +616,13 @@ namespace MWGui { MWWorld::Ptr ptr = mDragAndDrop->mItem.mBase; - mDragAndDrop->finish(); + auto [canEquipRes, canEquipMsg] = ptr.getClass().canBeEquipped(ptr, mPtr); + if (canEquipRes == 0) // cannot equip + { + mDragAndDrop->drop(mTradeModel, mItemView); // also plays down sound + MWBase::Environment::get().getWindowManager()->messageBox(canEquipMsg); + return; + } if (mDragAndDrop->mSourceModel != mTradeModel) { @@ -599,33 +631,7 @@ namespace MWGui mDragAndDrop->mItem, mDragAndDrop->mDraggedCount, mTradeModel); } - // Handles partial equipping - mEquippedStackableCount.reset(); - const auto slots = ptr.getClass().getEquipmentSlots(ptr); - if (!slots.first.empty() && slots.second) - { - MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator slotIt = invStore.getSlot(slots.first.front()); - - // Save the currently equipped count before useItem() - if (slotIt != invStore.end() && slotIt->getCellRef().getRefId() == ptr.getCellRef().getRefId()) - mEquippedStackableCount = slotIt->getCellRef().getCount(); - else - mEquippedStackableCount = 0; - } - MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false); - - // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 - // item - if ((ptr.getType() == ESM::Potion::sRecordId || ptr.getType() == ESM::Ingredient::sRecordId) - && mDragAndDrop->mDraggedCount > 1) - { - // Item can be provided from other window for example container. - // But after DragAndDrop::startDrag item automaticly always gets to player inventory. - mSelectedItem = getModel()->getIndex(mDragAndDrop->mItem); - dragItem(nullptr, mDragAndDrop->mDraggedCount - 1); - } } else { @@ -681,22 +687,21 @@ namespace MWGui void InventoryWindow::onFrame(float dt) { - updateEncumbranceBar(); - - if (mPinned) + if (mUpdateNextFrame) { - mUpdateTimer += dt; - if (0.1f < mUpdateTimer) + if (mTrading) { - mUpdateTimer = 0; - - // Update pinned inventory in-game - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) - { - mItemView->update(); - notifyContentChanged(); - } + mTradeModel->updateBorrowed(); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->mTradeModel->updateBorrowed(); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->updateItemView(); + MWBase::Environment::get().getWindowManager()->getTradeWindow()->updateOffer(); } + + updateEncumbranceBar(); + mDragAndDrop->update(); + mItemView->update(); + notifyContentChanged(); + mUpdateNextFrame = false; } } @@ -778,7 +783,16 @@ namespace MWGui if (mDragAndDrop->mIsOnDragAndDrop) mDragAndDrop->finish(); - mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); + if (MyGUI::InputManager::getInstance().isAltPressed()) + { + const MWWorld::Ptr item = mTradeModel->getItem(i).mBase; + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + mItemView->update(); + } + else + { + mDragAndDrop->startDrag(i, mSortModel, mTradeModel, mItemView, count); + } MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } @@ -848,6 +862,16 @@ namespace MWGui mPreview->rebuild(); } + void InventoryWindow::itemAdded(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + + void InventoryWindow::itemRemoved(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + MyGUI::IntSize InventoryWindow::getPreviewViewportSize() const { const MyGUI::IntSize previewWindowSize = mAvatarImage->getSize(); diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index 9fc77ceec5..e245fe46ca 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -5,8 +5,11 @@ #include "windowpinnablebase.hpp" #include "../mwrender/characterpreview.hpp" +#include "../mwworld/containerstore.hpp" #include "../mwworld/ptr.hpp" +#include + namespace osg { class Group; @@ -29,14 +32,18 @@ namespace MWGui class TradeItemModel; class DragAndDrop; class ItemModel; + class ItemTransfer; - class InventoryWindow : public WindowPinnableBase + class InventoryWindow : public WindowPinnableBase, public MWWorld::ContainerStoreListener { public: - InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem); + explicit InventoryWindow(DragAndDrop& dragAndDrop, ItemTransfer& itemTransfer, osg::Group* parent, + Resource::ResourceSystem* resourceSystem); void onOpen() override; + void onClose() override; + /// start trading, disables item drag&drop void setTrading(bool trading); @@ -62,6 +69,9 @@ namespace MWGui void setGuiMode(GuiMode mode); + void itemAdded(const MWWorld::ConstPtr& item, int count) override; + void itemRemoved(const MWWorld::ConstPtr& item, int count) override; + /// Cycle to previous/next weapon void cycle(bool next); @@ -71,10 +81,10 @@ namespace MWGui void onTitleDoubleClicked() override; private: - DragAndDrop* mDragAndDrop; + Misc::NotNullPtr mDragAndDrop; + Misc::NotNullPtr mItemTransfer; int mSelectedItem; - std::optional mEquippedStackableCount; MWWorld::Ptr mPtr; @@ -107,7 +117,7 @@ namespace MWGui std::unique_ptr mPreview; bool mTrading; - float mUpdateTimer; + bool mUpdateNextFrame; void toggleMaximized(); @@ -116,8 +126,9 @@ namespace MWGui void onBackgroundSelected(); - void sellItem(MyGUI::Widget* sender, int count); - void dragItem(MyGUI::Widget* sender, int count); + void sellItem(MyGUI::Widget* sender, std::size_t count); + void dragItem(MyGUI::Widget* sender, std::size_t count); + void transferItem(MyGUI::Widget* sender, std::size_t count); void onWindowResize(MyGUI::Window* _sender); void onFilterChanged(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/itemtransfer.hpp b/apps/openmw/mwgui/itemtransfer.hpp new file mode 100644 index 0000000000..fbd37bf136 --- /dev/null +++ b/apps/openmw/mwgui/itemtransfer.hpp @@ -0,0 +1,78 @@ +#ifndef OPENMW_APPS_OPENMW_MWGUI_ITEMTRANSFER_H +#define OPENMW_APPS_OPENMW_MWGUI_ITEMTRANSFER_H + +#include "inventorywindow.hpp" +#include "itemmodel.hpp" +#include "itemview.hpp" +#include "windowmanagerimp.hpp" +#include "worlditemmodel.hpp" + +#include +#include + +#include + +#include + +namespace MWGui +{ + class ItemTransfer + { + public: + explicit ItemTransfer(WindowManager& windowManager) + : mWindowManager(&windowManager) + { + } + + void addTarget(ItemView& view) { mTargets.insert(&view); } + + void removeTarget(ItemView& view) { mTargets.erase(&view); } + + void apply(const ItemStack& item, std::size_t count, ItemView& sourceView) + { + if (item.mFlags & ItemStack::Flag_Bound) + { + mWindowManager->messageBox("#{sBarterDialog12}"); + return; + } + + ItemView* targetView = nullptr; + + for (ItemView* const view : mTargets) + { + if (view == &sourceView) + continue; + + if (targetView != nullptr) + { + mWindowManager->messageBox("#{sContentsMessage2}"); + return; + } + + targetView = view; + } + + WorldItemModel worldItemModel(0.5f, 0.5f); + ItemModel* const targetModel = targetView == nullptr ? &worldItemModel : targetView->getModel(); + + if (!targetModel->onDropItem(item.mBase, count)) + return; + + sourceView.getModel()->moveItem(item, count, targetModel); + + if (targetView != nullptr) + targetView->update(); + + sourceView.update(); + + mWindowManager->getInventoryWindow()->updateItemView(); + mWindowManager->playSound(item.mBase.getClass().getDownSoundId(item.mBase)); + } + + private: + Misc::NotNullPtr mWindowManager; + std::unordered_set mTargets; + }; +} + +#endif diff --git a/apps/openmw/mwgui/itemview.hpp b/apps/openmw/mwgui/itemview.hpp index cfbc8a37ac..aeed0a9113 100644 --- a/apps/openmw/mwgui/itemview.hpp +++ b/apps/openmw/mwgui/itemview.hpp @@ -17,6 +17,8 @@ namespace MWGui /// Register needed components with MyGUI's factory manager static void registerComponents(); + ItemModel* getModel() { return mModel.get(); } + /// Takes ownership of \a model void setModel(std::unique_ptr model); diff --git a/apps/openmw/mwgui/jailscreen.cpp b/apps/openmw/mwgui/jailscreen.cpp index c6aefdd177..9244e9dd2f 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -4,6 +4,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -23,7 +24,6 @@ namespace MWGui : WindowBase("openmw_jail_screen.layout") , mDays(1) , mFadeTimeRemaining(0) - , mTimeAdvancer(0.01f) { getWidget(mProgressBar, "ProgressBar"); @@ -87,46 +87,6 @@ namespace MWGui // We should not worsen corprus when in prison player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); - - const auto& skillStore = MWBase::Environment::get().getESMStore()->get(); - std::set skills; - for (int day = 0; day < mDays; ++day) - { - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - const ESM::Skill* skill = skillStore.searchRandom({}, prng); - skills.insert(skill); - - MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill->mId); - if (skill->mId == ESM::Skill::Security || skill->mId == ESM::Skill::Sneak) - value.setBase(std::min(100.f, value.getBase() + 1)); - else - value.setBase(std::max(0.f, value.getBase() - 1)); - } - - const MWWorld::Store& gmst - = MWBase::Environment::get().getESMStore()->get(); - - std::string message; - if (mDays == 1) - message = gmst.find("sNotifyMessage42")->mValue.getString(); - else - message = gmst.find("sNotifyMessage43")->mValue.getString(); - - message = Misc::StringUtils::format(message, mDays); - - for (const ESM::Skill* skill : skills) - { - int skillValue = player.getClass().getNpcStats(player).getSkill(skill->mId).getBase(); - std::string skillMsg = gmst.find("sNotifyMessage44")->mValue.getString(); - if (skill->mId == ESM::Skill::Sneak || skill->mId == ESM::Skill::Security) - skillMsg = gmst.find("sNotifyMessage39")->mValue.getString(); - - skillMsg = Misc::StringUtils::format(skillMsg, skill->mName, skillValue); - message += "\n" + skillMsg; - } - - std::vector buttons; - buttons.emplace_back("#{Interface:OK}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); + MWBase::Environment::get().getLuaManager()->jailTimeServed(player, mDays); } } diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 263e676e15..8322ae9073 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -294,7 +294,7 @@ namespace MWGui if (!mGuiTexture.get()) { - mGuiTexture = std::make_unique(mTexture); + mGuiTexture = std::make_unique(mTexture); } if (!mCopyFramebufferToTextureCallback) diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index bf4bd7644c..59d21886dc 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -599,27 +599,27 @@ namespace MWGui osg::ref_ptr texture = mLocalMapRender->getMapTexture(entry.mCellX, entry.mCellY); if (texture) { - entry.mMapTexture = std::make_unique(texture); + entry.mMapTexture = std::make_unique(texture); entry.mMapWidget->setRenderItemTexture(entry.mMapTexture.get()); entry.mMapWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); needRedraw = true; } else - entry.mMapTexture = std::make_unique(std::string(), nullptr); + entry.mMapTexture = std::make_unique(std::string(), nullptr); } if (!entry.mFogTexture && mFogOfWarToggled && mFogOfWarEnabled) { osg::ref_ptr tex = mLocalMapRender->getFogOfWarTexture(entry.mCellX, entry.mCellY); if (tex) { - entry.mFogTexture = std::make_unique(tex); + entry.mFogTexture = std::make_unique(tex); entry.mFogWidget->setRenderItemTexture(entry.mFogTexture.get()); entry.mFogWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } else { entry.mFogWidget->setImageTexture("black"); - entry.mFogTexture = std::make_unique(std::string(), nullptr); + entry.mFogTexture = std::make_unique(std::string(), nullptr); } needRedraw = true; } @@ -1280,11 +1280,12 @@ namespace MWGui { if (!mGlobalMapTexture.get()) { - mGlobalMapTexture = std::make_unique(mGlobalMapRender->getBaseTexture()); + mGlobalMapTexture = std::make_unique(mGlobalMapRender->getBaseTexture()); mGlobalMapImage->setRenderItemTexture(mGlobalMapTexture.get()); mGlobalMapImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); - mGlobalMapOverlayTexture = std::make_unique(mGlobalMapRender->getOverlayTexture()); + mGlobalMapOverlayTexture + = std::make_unique(mGlobalMapRender->getOverlayTexture()); mGlobalMapOverlay->setRenderItemTexture(mGlobalMapOverlayTexture.get()); mGlobalMapOverlay->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp index 7712594c54..4513bea9da 100644 --- a/apps/openmw/mwgui/postprocessorhud.cpp +++ b/apps/openmw/mwgui/postprocessorhud.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -33,6 +34,14 @@ namespace MWGui { + namespace + { + std::shared_ptr& getTechnique(const MyGUI::ListBox& list, size_t selected) + { + return *list.getItemDataAt>(selected); + } + } + void PostProcessorHud::ListWrapper::onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch) { if (MyGUI::InputManager::getInstance().isShiftPressed() @@ -42,8 +51,9 @@ namespace MWGui MyGUI::ListBox::onKeyButtonPressed(key, ch); } - PostProcessorHud::PostProcessorHud() + PostProcessorHud::PostProcessorHud(Files::ConfigurationManager& cfgMgr) : WindowBase("openmw_postprocessor_hud.layout") + , mCfgMgr(cfgMgr) { getWidget(mActiveList, "ActiveList"); getWidget(mInactiveList, "InactiveList"); @@ -102,7 +112,7 @@ namespace MWGui { for (size_t i = 1; i < mConfigArea->getChildCount(); ++i) { - if (auto* child = dynamic_cast(mConfigArea->getChildAt(i))) + if (auto* child = dynamic_cast(mConfigArea->getChildAt(i))) child->toDefault(); } } @@ -117,7 +127,7 @@ namespace MWGui if (index >= sender->getItemCount()) return; - updateConfigView(sender->getItemNameAt(index)); + updateConfigView(getTechnique(*sender, index)->getFileName()); } void PostProcessorHud::toggleTechnique(bool enabled) @@ -131,7 +141,7 @@ namespace MWGui auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); mOverrideHint = list->getItemNameAt(selected); - auto technique = *list->getItemDataAt>(selected); + auto technique = getTechnique(*list, selected); if (technique->getDynamic()) return; @@ -167,7 +177,7 @@ namespace MWGui if (static_cast(index) != selected) { - auto technique = *mActiveList->getItemDataAt>(selected); + auto technique = getTechnique(*mActiveList, selected); if (technique->getDynamic() || technique->getInternal()) return; @@ -235,6 +245,8 @@ namespace MWGui void PostProcessorHud::onClose() { + Settings::ShaderManager::get().save(); + Settings::Manager::saveUser(mCfgMgr.getUserConfigPath() / "settings.cfg"); toggleMode(Settings::ShaderManager::Mode::Normal); } @@ -290,18 +302,18 @@ namespace MWGui return; if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE) - updateConfigView(mInactiveList->getItemNameAt(mInactiveList->getIndexSelected())); + updateConfigView(getTechnique(*mInactiveList, mInactiveList->getIndexSelected())->getFileName()); else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE) - updateConfigView(mActiveList->getItemNameAt(mActiveList->getIndexSelected())); + updateConfigView(getTechnique(*mActiveList, mActiveList->getIndexSelected())->getFileName()); } - void PostProcessorHud::updateConfigView(const std::string& name) + void PostProcessorHud::updateConfigView(VFS::Path::NormalizedView path) { auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); - auto technique = processor->loadTechnique(name); + auto technique = processor->loadTechnique(path); - if (technique->getStatus() == fx::Technique::Status::File_Not_exists) + if (technique->getStatus() == Fx::Technique::Status::File_Not_exists) return; while (mConfigArea->getChildCount() > 0) @@ -322,15 +334,15 @@ namespace MWGui const auto flags = technique->getFlags(); - const auto flag_interior = serializeBool(!(flags & fx::Technique::Flag_Disable_Interiors)); - const auto flag_exterior = serializeBool(!(flags & fx::Technique::Flag_Disable_Exteriors)); - const auto flag_underwater = serializeBool(!(flags & fx::Technique::Flag_Disable_Underwater)); - const auto flag_abovewater = serializeBool(!(flags & fx::Technique::Flag_Disable_Abovewater)); + const auto flag_interior = serializeBool(!(flags & Fx::Technique::Flag_Disable_Interiors)); + const auto flag_exterior = serializeBool(!(flags & Fx::Technique::Flag_Disable_Exteriors)); + const auto flag_underwater = serializeBool(!(flags & Fx::Technique::Flag_Disable_Underwater)); + const auto flag_abovewater = serializeBool(!(flags & Fx::Technique::Flag_Disable_Abovewater)); switch (technique->getStatus()) { - case fx::Technique::Status::Success: - case fx::Technique::Status::Uncompiled: + case Fx::Technique::Status::Success: + case Fx::Technique::Status::Uncompiled: { if (technique->getDynamic()) ss << "#{fontcolourhtml=header}#{OMWShaders:ShaderLocked}: #{fontcolourhtml=normal} " @@ -352,13 +364,13 @@ namespace MWGui << flag_abovewater; break; } - case fx::Technique::Status::Parse_Error: + case Fx::Technique::Status::Parse_Error: ss << "#{fontcolourhtml=negative}Shader Compile Error: #{fontcolourhtml=normal} <" << std::string(technique->getName()) << "> failed to compile." << endl << endl << technique->getLastError(); break; - case fx::Technique::Status::File_Not_exists: + case Fx::Technique::Status::File_Not_exists: break; } @@ -390,7 +402,7 @@ namespace MWGui divider->setCaptionWithReplacing(uniform->mHeader); } - fx::Widgets::UniformBase* uwidget = mConfigArea->createWidget( + Fx::Widgets::UniformBase* uwidget = mConfigArea->createWidget( "MW_UniformEdit", { 0, 0, 0, 22 }, MyGUI::Align::Default); uwidget->init(uniform); uwidget->getLabel()->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel); @@ -423,22 +435,22 @@ namespace MWGui auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor(); - std::vector techniques; - for (const auto& [name, _] : processor->getTechniqueMap()) - techniques.push_back(name); - std::sort(techniques.begin(), techniques.end(), Misc::StringUtils::ciLess); + std::vector techniques; + for (const auto& vfsPath : processor->getTechniqueFiles()) + techniques.emplace_back(vfsPath); + std::sort(techniques.begin(), techniques.end()); - for (const std::string& name : techniques) + for (VFS::Path::NormalizedView path : techniques) { - auto technique = processor->loadTechnique(name); + auto technique = processor->loadTechnique(path); if (!technique->getHidden() && !processor->isTechniqueEnabled(technique)) { - std::string lowerName = Utf8Stream::lowerCaseUtf8(name); + std::string lowerName = Utf8Stream::lowerCaseUtf8(technique->getName()); std::string lowerCaption = mFilter->getCaption(); lowerCaption = Utf8Stream::lowerCaseUtf8(lowerCaption); if (lowerName.find(lowerCaption) != std::string::npos) - mInactiveList->addItem(name, technique); + mInactiveList->addItem(technique->getName(), technique); } } @@ -481,14 +493,14 @@ namespace MWGui void PostProcessorHud::registerMyGUIComponents() { MyGUI::FactoryManager& factory = MyGUI::FactoryManager::getInstance(); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); - factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); + factory.registerFactory("Widget"); factory.registerFactory("Widget"); } } diff --git a/apps/openmw/mwgui/postprocessorhud.hpp b/apps/openmw/mwgui/postprocessorhud.hpp index 20e27bac3a..b5cf2495a6 100644 --- a/apps/openmw/mwgui/postprocessorhud.hpp +++ b/apps/openmw/mwgui/postprocessorhud.hpp @@ -5,7 +5,9 @@ #include +#include #include +#include namespace MyGUI { @@ -31,7 +33,7 @@ namespace MWGui }; public: - PostProcessorHud(); + PostProcessorHud(Files::ConfigurationManager& cfgMgr); void onOpen() override; @@ -48,7 +50,7 @@ namespace MWGui void notifyFilterChanged(MyGUI::EditBox* sender); - void updateConfigView(const std::string& name); + void updateConfigView(VFS::Path::NormalizedView path); void notifyResetButtonClicked(MyGUI::Widget* sender); @@ -98,6 +100,8 @@ namespace MWGui std::string mOverrideHint; int mOffset = 0; + + Files::ConfigurationManager& mCfgMgr; }; } diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index c8932c97b6..3c62400e0d 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -84,9 +84,8 @@ namespace MWGui case ESM::QuickKeys::Type::MagicItem: { MWWorld::Ptr item = *mKey[index].button->getUserData(); - // Make sure the item is available and is not broken - if (item.isEmpty() || item.getCellRef().getCount() < 1 - || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + // Make sure the item is available + if (item.isEmpty() || item.getCellRef().getCount() < 1) { // Try searching for a compatible replacement item = store.findReplacement(mKey[index].id); @@ -229,7 +228,7 @@ namespace MWGui mAssignDialog->setVisible(false); } - void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) + void QuickKeysMenu::assignItem(MWWorld::Ptr item) { assert(mSelected); @@ -248,6 +247,12 @@ namespace MWGui mItemSelectionDialog->setVisible(false); } + void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) + { + assignItem(item); + MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); + } + void QuickKeysMenu::onAssignItemCancel() { mItemSelectionDialog->setVisible(false); @@ -382,23 +387,16 @@ namespace MWGui if (it == store.end()) item = nullptr; - // check the item is available and not broken - if (item.isEmpty() || item.getCellRef().getCount() < 1 - || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + // check the quickkey item is available + if (item.isEmpty() || item.getCellRef().getCount() < 1) { - item = store.findReplacement(key->id); - - if (item.isEmpty() || item.getCellRef().getCount() < 1) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); - - return; - } + MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); + return; } if (key->type == ESM::QuickKeys::Type::Item) { - if (!store.isEquipped(item)) + if (!store.isEquipped(item.getCellRef().getRefId())) MWBase::Environment::get().getWindowManager()->useItem(item); MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); @@ -572,7 +570,7 @@ namespace MWGui else { if (quickKey.mType == ESM::QuickKeys::Type::Item) - onAssignItem(item); + assignItem(item); else // if (quickKey.mType == ESM::QuickKeys::Type::MagicItem) onAssignMagicItem(item); } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 904029b9a0..a43cce50b4 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -72,6 +72,7 @@ namespace MWGui // Check if quick key is still valid inline void validate(int index); void unassign(keyData* key); + void assignItem(MWWorld::Ptr item); }; class QuickKeysMenuAssign : public WindowModal diff --git a/apps/openmw/mwgui/race.cpp b/apps/openmw/mwgui/race.cpp index 7b445d419f..c7de8f4125 100644 --- a/apps/openmw/mwgui/race.cpp +++ b/apps/openmw/mwgui/race.cpp @@ -154,7 +154,7 @@ namespace MWGui mPreview->setAngle(mCurrentAngle); mPreviewTexture - = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); + = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreviewImage->setRenderItemTexture(mPreviewTexture.get()); mPreviewImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index 4957789ffd..eec3b7bfe6 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -488,7 +488,7 @@ namespace MWGui texture->setResizeNonPowerOfTwoHint(false); texture->setUnRefImageDataAfterApply(true); - mScreenshotTexture = std::make_unique(texture); + mScreenshotTexture = std::make_unique(texture); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); } } diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 02353c5d41..77032623d2 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwlua/luamanagerimp.hpp" #include "confirmationdialog.hpp" @@ -247,10 +249,11 @@ namespace MWGui } } - SettingsWindow::SettingsWindow() + SettingsWindow::SettingsWindow(Files::ConfigurationManager& cfgMgr) : WindowBase("openmw_settings_window.layout") , mKeyboardMode(true) , mCurrentPage(-1) + , mCfgMgr(cfgMgr) { const bool terrain = Settings::terrain().mDistantTerrain; const std::string_view widgetName = terrain ? "RenderingDistanceSlider" : "LargeRenderingDistanceSlider"; @@ -1092,6 +1095,14 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } + void SettingsWindow::onClose() + { + // Save user settings + Settings::Manager::saveUser(mCfgMgr.getUserConfigPath() / "settings.cfg"); + MWBase::Environment::get().getLuaManager()->savePermanentStorage(mCfgMgr.getUserConfigPath()); + MWBase::Environment::get().getInputManager()->saveBindings(); + } + void SettingsWindow::onWindowResize(MyGUI::Window* _sender) { layoutControlsBox(); diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index dc4e09f8ac..22a15eab97 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -1,6 +1,7 @@ #ifndef MWGUI_SETTINGS_H #define MWGUI_SETTINGS_H +#include #include #include "windowbase.hpp" @@ -10,10 +11,12 @@ namespace MWGui class SettingsWindow : public WindowBase { public: - SettingsWindow(); + SettingsWindow(Files::ConfigurationManager& cfgMgr); void onOpen() override; + void onClose() override; + void onFrame(float duration) override; void updateControlsBox(); @@ -120,6 +123,7 @@ namespace MWGui private: void resetScrollbars(); + Files::ConfigurationManager& mCfgMgr; }; } diff --git a/apps/openmw/mwgui/sortfilteritemmodel.cpp b/apps/openmw/mwgui/sortfilteritemmodel.cpp index fe85ea4bd0..8c6277db4d 100644 --- a/apps/openmw/mwgui/sortfilteritemmodel.cpp +++ b/apps/openmw/mwgui/sortfilteritemmodel.cpp @@ -279,7 +279,7 @@ namespace MWGui && !base.get()->mBase->mData.mIsScroll) return false; - if ((mFilter & Filter_OnlyUsableItems) && base.getClass().getScript(base).empty()) + if ((mFilter & Filter_OnlyUsableItems)) { std::unique_ptr actionOnUse = base.getClass().use(base); if (!actionOnUse || actionOnUse->isNullAction()) diff --git a/apps/openmw/mwgui/timeadvancer.cpp b/apps/openmw/mwgui/timeadvancer.cpp index 2cdab127b9..c4bdc030c2 100644 --- a/apps/openmw/mwgui/timeadvancer.cpp +++ b/apps/openmw/mwgui/timeadvancer.cpp @@ -1,14 +1,19 @@ #include "timeadvancer.hpp" +namespace +{ + // Time per hour tick + constexpr float kProgressStepDelay = 1.0f / 60.0f; +} + namespace MWGui { - TimeAdvancer::TimeAdvancer(float delay) + TimeAdvancer::TimeAdvancer() : mRunning(false) , mCurHour(0) , mHours(1) , mInterruptAt(-1) - , mDelay(delay) - , mRemainingTime(delay) + , mRemainingTime(kProgressStepDelay) { } @@ -17,7 +22,7 @@ namespace MWGui mHours = hours; mCurHour = 0; mInterruptAt = interruptAt; - mRemainingTime = mDelay; + mRemainingTime = kProgressStepDelay; mRunning = true; } @@ -43,7 +48,7 @@ namespace MWGui while (mRemainingTime <= 0) { - mRemainingTime += mDelay; + mRemainingTime += kProgressStepDelay; ++mCurHour; if (mCurHour <= mHours) diff --git a/apps/openmw/mwgui/timeadvancer.hpp b/apps/openmw/mwgui/timeadvancer.hpp index bb6aa649cb..e69153aed4 100644 --- a/apps/openmw/mwgui/timeadvancer.hpp +++ b/apps/openmw/mwgui/timeadvancer.hpp @@ -8,7 +8,7 @@ namespace MWGui class TimeAdvancer { public: - TimeAdvancer(float delay); + TimeAdvancer(); void run(int hours, int interruptAt = -1); void stop(); @@ -32,7 +32,6 @@ namespace MWGui int mHours; int mInterruptAt; - float mDelay; float mRemainingTime; }; } diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 28f0b80010..7f8de572ed 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -894,7 +894,8 @@ namespace MWGui widget->setUserString("ToolTipLayout", "BirthSignToolTip"); widget->setUserString( "ImageTexture_BirthSignImage", Misc::ResourceHelpers::correctTexturePath(sign->mTexture, vfs)); - std::string text = sign->mName + "\n#{fontcolourhtml=normal}" + sign->mDescription; + widget->setUserString("Caption_BirthSignName", sign->mName); + widget->setUserString("Caption_BirthSignDescription", sign->mDescription); std::vector abilities, powers, spells; @@ -915,26 +916,22 @@ namespace MWGui spells.push_back(spell); } - using Category = std::pair&, std::string_view>; - for (const auto& [category, label] : std::initializer_list{ - { abilities, "sBirthsignmenu1" }, { powers, "sPowers" }, { spells, "sBirthsignmenu2" } }) + using Category = std::tuple&, std::string_view, std::string_view>; + std::initializer_list categories{ { abilities, "#{sBirthsignmenu1}", "Abilities" }, + { powers, "#{sPowers}", "Powers" }, { spells, "#{sBirthsignmenu2}", "Spells" } }; + + for (const auto& [category, label, widgetName] : categories) { - bool addHeader = true; - for (const ESM::Spell* spell : category) + std::string text; + if (!category.empty()) { - if (addHeader) - { - text += "\n\n#{fontcolourhtml=header}#{"; - text += label; - text += '}'; - addHeader = false; - } - - text += "\n#{fontcolourhtml=normal}" + spell->mName; + text = std::string(label) + "\n#{fontcolourhtml=normal}"; + for (const ESM::Spell* spell : category) + text += spell->mName + ' '; + text.pop_back(); } + widget->setUserString("Caption_BirthSign" + std::string(widgetName), text); } - - widget->setUserString("Caption_BirthSignText", text); } void ToolTips::createRaceToolTip(MyGUI::Widget* widget, const ESM::Race* playerRace) diff --git a/apps/openmw/mwgui/tradeitemmodel.cpp b/apps/openmw/mwgui/tradeitemmodel.cpp index 50a55f5061..660e940367 100644 --- a/apps/openmw/mwgui/tradeitemmodel.cpp +++ b/apps/openmw/mwgui/tradeitemmodel.cpp @@ -113,6 +113,25 @@ namespace MWGui encumbrance = std::max(0.f, encumbrance); } + void TradeItemModel::updateBorrowed() + { + auto update = [](std::vector& list) { + for (auto it = list.begin(); it != list.end();) + { + size_t actualCount = it->mBase.getCellRef().getCount(); + if (actualCount < it->mCount) + it->mCount = actualCount; + if (it->mCount == 0) + it = list.erase(it); + else + ++it; + } + }; + + update(mBorrowedFromUs); + update(mBorrowedToUs); + } + void TradeItemModel::abort() { mBorrowedFromUs.clear(); diff --git a/apps/openmw/mwgui/tradeitemmodel.hpp b/apps/openmw/mwgui/tradeitemmodel.hpp index d395744d2a..856f33563d 100644 --- a/apps/openmw/mwgui/tradeitemmodel.hpp +++ b/apps/openmw/mwgui/tradeitemmodel.hpp @@ -31,6 +31,9 @@ namespace MWGui void returnItemBorrowedFromUs(ModelIndex itemIndex, ItemModel* source, size_t count); + /// Update borrowed items in this model + void updateBorrowed(); + /// Permanently transfers items that were borrowed to us from another model to this model void transferItems(); /// Aborts trade diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index ba752303d2..bf5d4d4279 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -123,6 +123,7 @@ namespace MWGui , mItemToSell(-1) , mCurrentBalance(0) , mCurrentMerchantOffer(0) + , mUpdateNextFrame(false) { getWidget(mFilterAll, "AllButton"); getWidget(mFilterWeapon, "WeaponButton"); @@ -201,11 +202,24 @@ namespace MWGui onFilterChanged(mFilterAll); mFilterEdit->setCaption({}); + + for (const auto& source : itemSources) + source.getClass().getContainerStore(source).setContListener(this); } void TradeWindow::onFrame(float dt) { checkReferenceAvailable(); + + if (isVisible() && mUpdateNextFrame) + { + mTradeModel->updateBorrowed(); + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel()->updateBorrowed(); + MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); + mItemView->update(); + updateOffer(); + mUpdateNextFrame = false; + } } void TradeWindow::onNameFilterChanged(MyGUI::EditBox* _sender) @@ -278,7 +292,7 @@ namespace MWGui } } - void TradeWindow::sellItem(MyGUI::Widget* sender, int count) + void TradeWindow::sellItem(MyGUI::Widget* /*sender*/, std::size_t count) { const ItemStack& item = mTradeModel->getItem(mItemToSell); const ESM::RefId& sound = item.mBase.getClass().getUpSoundId(item.mBase); @@ -643,4 +657,19 @@ namespace MWGui if (mTradeModel && mTradeModel->usesContainer(ptr)) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } + + void TradeWindow::updateItemView() + { + mItemView->update(); + } + + void TradeWindow::itemAdded(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } + + void TradeWindow::itemRemoved(const MWWorld::ConstPtr& item, int count) + { + mUpdateNextFrame = true; + } } diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp index 33c39cb269..5a3889d2d8 100644 --- a/apps/openmw/mwgui/tradewindow.hpp +++ b/apps/openmw/mwgui/tradewindow.hpp @@ -4,6 +4,8 @@ #include "referenceinterface.hpp" #include "windowbase.hpp" +#include "../mwworld/containerstore.hpp" + namespace Gui { class NumericEditBox; @@ -20,7 +22,7 @@ namespace MWGui class SortFilterItemModel; class TradeItemModel; - class TradeWindow : public WindowBase, public ReferenceInterface + class TradeWindow : public WindowBase, public ReferenceInterface, public MWWorld::ContainerStoreListener { public: TradeWindow(); @@ -31,23 +33,25 @@ namespace MWGui void onFrame(float dt) override; void clear() override { resetReference(); } - void borrowItem(int index, size_t count); - void returnItem(int index, size_t count); - - int getMerchantServices(); - bool exit() override; void resetReference() override; void onDeleteCustomData(const MWWorld::Ptr& ptr) override; + void updateItemView(); + + void itemAdded(const MWWorld::ConstPtr& item, int count) override; + void itemRemoved(const MWWorld::ConstPtr& item, int count) override; + typedef MyGUI::delegates::MultiDelegate<> EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; std::string_view getWindowIdForLua() const override { return "Trade"; } private: + friend class InventoryWindow; + ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; @@ -81,6 +85,8 @@ namespace MWGui int mCurrentBalance; int mCurrentMerchantOffer; + bool mUpdateNextFrame; + void sellToNpc( const MWWorld::Ptr& item, int count, bool boughtItem); ///< only used for adjusting the gold balance void buyFromNpc( @@ -89,7 +95,12 @@ namespace MWGui void updateOffer(); void onItemSelected(int index); - void sellItem(MyGUI::Widget* sender, int count); + void sellItem(MyGUI::Widget* sender, std::size_t count); + + void borrowItem(int index, size_t count); + void returnItem(int index, size_t count); + + int getMerchantServices(); void onFilterChanged(MyGUI::Widget* _sender); void onNameFilterChanged(MyGUI::EditBox* _sender); diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 890aa0ba68..3fc8412d4c 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -27,10 +27,9 @@ namespace MWGui TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") - , mTimeAdvancer(0.05f) { getWidget(mTrainingOptions, "TrainingOptions"); - getWidget(mCancelButton, "CancelButton"); + getWidget(mCancelButton, "OkButton"); getWidget(mPlayerGold, "PlayerGold"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onCancelButtonClicked); @@ -116,14 +115,14 @@ namespace MWGui MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip - MyGUI::IntCoord(5, 5 + i * lineHeight, mTrainingOptions->getWidth() - 10, lineHeight), + MyGUI::IntCoord(4, 3 + i * lineHeight, mTrainingOptions->getWidth() - 10, lineHeight), MyGUI::Align::Default); button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); button->setCaptionWithReplacing( - MyGUI::TextIterator::toTagsString(skill->mName) + " - " + MyGUI::utility::toString(price)); + MyGUI::TextIterator::toTagsString(skill->mName) + " - " + MyGUI::utility::toString(price) + "#{sgp}"); button->setSize(button->getTextSize().width + 12, button->getSize().height); diff --git a/apps/openmw/mwgui/videowidget.cpp b/apps/openmw/mwgui/videowidget.cpp index a82d8ce67f..0fc555ab27 100644 --- a/apps/openmw/mwgui/videowidget.cpp +++ b/apps/openmw/mwgui/videowidget.cpp @@ -50,7 +50,7 @@ namespace MWGui if (!texture) return; - mTexture = std::make_unique(texture); + mTexture = std::make_unique(texture); setRenderItemTexture(mTexture.get()); getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 222a34e53b..9609def96d 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -52,7 +52,6 @@ namespace MWGui WaitDialog::WaitDialog() : WindowBase("openmw_wait_dialog.layout") - , mTimeAdvancer(0.05f) , mSleeping(false) , mHours(1) , mManualHours(1) diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 565fb43127..bfacbd7e68 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -93,6 +93,7 @@ #include "hud.hpp" #include "inventorywindow.hpp" #include "itemchargeview.hpp" +#include "itemtransfer.hpp" #include "itemview.hpp" #include "itemwidget.hpp" #include "jailscreen.hpp" @@ -204,7 +205,7 @@ namespace MWGui SDL_GL_GetDrawableSize(window, &dw, &dh); mScalingFactor = Settings::gui().mScalingFactor * (dw / w); - mGuiPlatform = std::make_unique(viewer, guiRoot, resourceSystem->getImageManager(), + mGuiPlatform = std::make_unique(viewer, guiRoot, resourceSystem->getImageManager(), resourceSystem->getVFS(), mScalingFactor, "mygui", logpath / "MyGUI.log"); mGui = std::make_unique(); @@ -228,8 +229,8 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); - MyGUI::FactoryManager::getInstance().registerFactory("Layer"); - MyGUI::FactoryManager::getInstance().registerFactory("Layer"); + MyGUI::FactoryManager::getInstance().registerFactory("Layer"); + MyGUI::FactoryManager::getInstance().registerFactory("Layer"); BookPage::registerMyGUIComponents(); PostProcessorHud::registerMyGUIComponents(); ItemView::registerComponents(); @@ -312,6 +313,7 @@ namespace MWGui mTextColours.loadColours(); mDragAndDrop = std::make_unique(); + mItemTransfer = std::make_unique(*this); auto recharge = std::make_unique(); mGuiModeStates[GM_Recharge] = GuiModeState(recharge.get()); @@ -334,7 +336,7 @@ namespace MWGui trackWindow(mStatsWindow, makeStatsWindowSettingValues()); auto inventoryWindow = std::make_unique( - mDragAndDrop.get(), mViewer->getSceneData()->asGroup(), mResourceSystem); + *mDragAndDrop, *mItemTransfer, mViewer->getSceneData()->asGroup(), mResourceSystem); mInventoryWindow = inventoryWindow.get(); mWindows.push_back(std::move(inventoryWindow)); @@ -381,7 +383,7 @@ namespace MWGui mGuiModeStates[GM_Dialogue] = GuiModeState(mDialogueWindow); mTradeWindow->eventTradeDone += MyGUI::newDelegate(mDialogueWindow, &DialogueWindow::onTradeComplete); - auto containerWindow = std::make_unique(mDragAndDrop.get()); + auto containerWindow = std::make_unique(*mDragAndDrop, *mItemTransfer); mContainerWindow = containerWindow.get(); mWindows.push_back(std::move(containerWindow)); trackWindow(mContainerWindow, makeContainerWindowSettingValues()); @@ -407,7 +409,7 @@ namespace MWGui mCountDialog = countDialog.get(); mWindows.push_back(std::move(countDialog)); - auto settingsWindow = std::make_unique(); + auto settingsWindow = std::make_unique(mCfgMgr); mSettingsWindow = settingsWindow.get(); mWindows.push_back(std::move(settingsWindow)); trackWindow(mSettingsWindow, makeSettingsWindowSettingValues()); @@ -457,7 +459,8 @@ namespace MWGui mSoulgemDialog = std::make_unique(mMessageBoxManager.get()); - auto companionWindow = std::make_unique(mDragAndDrop.get(), mMessageBoxManager.get()); + auto companionWindow + = std::make_unique(*mDragAndDrop, *mItemTransfer, mMessageBoxManager.get()); trackWindow(companionWindow.get(), makeCompanionWindowSettingValues()); mGuiModeStates[GM_Companion] = GuiModeState({ mInventoryWindow, companionWindow.get() }); mWindows.push_back(std::move(companionWindow)); @@ -500,7 +503,7 @@ namespace MWGui mWindows.push_back(std::move(debugWindow)); trackWindow(mDebugWindow, makeDebugWindowSettingValues()); - auto postProcessorHud = std::make_unique(); + auto postProcessorHud = std::make_unique(mCfgMgr); mPostProcessorHud = postProcessorHud.get(); mWindows.push_back(std::move(postProcessorHud)); trackWindow(mPostProcessorHud, makePostprocessorWindowSettingValues()); @@ -1124,7 +1127,7 @@ namespace MWGui std::vector split; Misc::StringUtils::split(tag, split, ":"); - l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager(); + L10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager(); // If a key has a "Context:KeyName" format, use YAML to translate data if (split.size() == 2) @@ -2406,6 +2409,14 @@ namespace MWGui updateVisible(); } + bool WindowManager::isWindowVisible(std::string_view windowId) const + { + auto it = mLuaIdToWindow.find(windowId); + if (it == mLuaIdToWindow.end()) + throw std::logic_error("Invalid window name: " + std::string(windowId)); + return it->second->isVisible(); + } + std::vector WindowManager::getAllWindowIds() const { std::vector res; diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 650e2bab78..c231718db6 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -8,7 +8,6 @@ **/ #include -#include #include #include @@ -118,6 +117,7 @@ namespace MWGui class PostProcessorHud; class JailScreen; class KeyboardNavigation; + class ItemTransfer; class WindowManager : public MWBase::WindowManager { @@ -390,6 +390,7 @@ namespace MWGui // Used in Lua bindings const std::vector& getGuiModeStack() const override { return mGuiModes; } void setDisabledByLua(std::string_view windowId, bool disabled) override; + bool isWindowVisible(std::string_view windowId) const override; std::vector getAllWindowIds() const override; std::vector getAllowedWindowIds(GuiMode mode) const override; @@ -401,7 +402,7 @@ namespace MWGui Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; - std::unique_ptr mGuiPlatform; + std::unique_ptr mGuiPlatform; osgViewer::Viewer* mViewer; std::unique_ptr mFontLoader; @@ -432,6 +433,7 @@ namespace MWGui Console* mConsole; DialogueWindow* mDialogueWindow; std::unique_ptr mDragAndDrop; + std::unique_ptr mItemTransfer; InventoryWindow* mInventoryWindow; ScrollWindow* mScrollWindow; BookWindow* mBookWindow; diff --git a/apps/openmw/mwgui/worlditemmodel.hpp b/apps/openmw/mwgui/worlditemmodel.hpp new file mode 100644 index 0000000000..137062eeb4 --- /dev/null +++ b/apps/openmw/mwgui/worlditemmodel.hpp @@ -0,0 +1,82 @@ +#ifndef OPENMW_APPS_OPENMW_MWGUI_WORLDITEMMODEL_H +#define OPENMW_APPS_OPENMW_MWGUI_WORLDITEMMODEL_H + +#include "itemmodel.hpp" + +#include +#include + +#include + +#include +#include + +#include + +namespace MWGui +{ + // Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. + class WorldItemModel : public ItemModel + { + public: + explicit WorldItemModel(float cursorX, float cursorY) + : mCursorX(cursorX) + , mCursorY(cursorY) + { + } + + MWWorld::Ptr dropItemImpl(const ItemStack& item, size_t count, bool copy) + { + MWBase::World& world = *MWBase::Environment::get().getWorld(); + + const MWWorld::Ptr player = world.getPlayerPtr(); + + world.breakInvisibility(player); + + const MWWorld::Ptr dropped = world.canPlaceObject(mCursorX, mCursorY) + ? world.placeObject(item.mBase, mCursorX, mCursorY, count, copy) + : world.dropObjectOnGround(player, item.mBase, count, copy); + + dropped.getCellRef().setOwner(ESM::RefId()); + + return dropped; + } + + MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override + { + return dropItemImpl(item, count, false); + } + + MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override + { + return dropItemImpl(item, count, true); + } + + void removeItem(const ItemStack& /*item*/, size_t /*count*/) override + { + throw std::runtime_error("WorldItemModel::removeItem is not implemented"); + } + + ModelIndex getIndex(const ItemStack& /*item*/) override + { + throw std::runtime_error("WorldItemModel::getIndex is not implemented"); + } + + void update() override {} + + size_t getItemCount() override { return 0; } + + ItemStack getItem(ModelIndex /*index*/) override + { + throw std::runtime_error("WorldItemModel::getItem is not implemented"); + } + + bool usesContainer(const MWWorld::Ptr&) override { return false; } + + private: + float mCursorX; + float mCursorY; + }; +} + +#endif diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 339ebf4276..22322014d4 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -196,23 +196,7 @@ namespace MWInput BindingsManager::~BindingsManager() { - const std::string newFileName = Files::pathToUnicodeString(mUserFile) + ".new"; - try - { - if (mInputBinder->save(newFileName)) - { - std::filesystem::rename(Files::pathFromUnicodeString(newFileName), mUserFile); - Log(Debug::Info) << "Saved input bindings: " << mUserFile; - } - else - { - Log(Debug::Error) << "Failed to save input bindings to " << newFileName; - } - } - catch (const std::exception& e) - { - Log(Debug::Error) << "Failed to save input bindings to " << newFileName << ": " << e.what(); - } + saveBindings(); } void BindingsManager::update(float dt) @@ -715,4 +699,25 @@ namespace MWInput if (previousValue <= 0.6 && currentValue > 0.6) manager->executeAction(action); } + + void BindingsManager::saveBindings() + { + const std::string newFileName = Files::pathToUnicodeString(mUserFile) + ".new"; + try + { + if (mInputBinder->save(newFileName)) + { + std::filesystem::rename(Files::pathFromUnicodeString(newFileName), mUserFile); + Log(Debug::Info) << "Saved input bindings: " << mUserFile; + } + else + { + Log(Debug::Error) << "Failed to save input bindings to " << newFileName; + } + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Failed to save input bindings to " << newFileName << ": " << e.what(); + } + } } diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index bee9e07cf7..40c2076d3c 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -65,6 +65,8 @@ namespace MWInput void actionValueChanged(int action, float currentValue, float previousValue); + void saveBindings(); + private: void setupSDLKeyMappings(); diff --git a/apps/openmw/mwinput/controllermanager.cpp b/apps/openmw/mwinput/controllermanager.cpp index 1a8490d8b7..0bba8bfa32 100644 --- a/apps/openmw/mwinput/controllermanager.cpp +++ b/apps/openmw/mwinput/controllermanager.cpp @@ -349,7 +349,6 @@ namespace MWInput void ControllerManager::enableGyroSensor() { mGyroAvailable = false; -#if SDL_VERSION_ATLEAST(2, 0, 14) SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); if (!cntrl) return; @@ -361,7 +360,6 @@ namespace MWInput return; } mGyroAvailable = true; -#endif } bool ControllerManager::isGyroAvailable() const @@ -372,7 +370,6 @@ namespace MWInput std::array ControllerManager::getGyroValues() const { float gyro[3] = { 0.f }; -#if SDL_VERSION_ATLEAST(2, 0, 14) SDL_GameController* cntrl = mBindingsManager->getControllerOrNull(); if (cntrl && mGyroAvailable) { @@ -380,7 +377,6 @@ namespace MWInput if (result < 0) Log(Debug::Error) << "Failed to get game controller sensor data: " << SDL_GetError(); } -#endif return std::array({ gyro[0], gyro[1], gyro[2] }); } diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 328757a954..d81d720b21 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -246,4 +246,9 @@ namespace MWInput { mActionManager->executeAction(action); } + + void InputManager::saveBindings() + { + mBindingsManager->saveBindings(); + } } diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 39a1133db5..46e4774b6b 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -106,6 +106,8 @@ namespace MWInput private: bool mControlsDisabled; + void saveBindings() override; + std::unique_ptr mInputWrapper; std::unique_ptr mBindingsManager; std::unique_ptr mControlSwitch; diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index 933dba3fda..ec64a3cddd 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -61,6 +61,10 @@ namespace sol struct is_automagical : std::false_type { }; + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua @@ -126,6 +130,14 @@ namespace MWLua return sol::nullopt; }); + cellT["pathGrid"] = sol::readonly_property([](const CellT& c) -> const ESM::Pathgrid* { + const ESM::Pathgrid* grid + = MWBase::Environment::get().getESMStore()->get().search(*c.mStore->getCell()); + if (grid && grid->mPoints.empty()) + return nullptr; + return grid; + }); + if constexpr (std::is_same_v) { // only for global scripts cellT["getAll"] = [ids = getPackageToTypeTable(view)](const CellT& cell, sol::optional type) { @@ -286,6 +298,34 @@ namespace MWLua return GObjectList{ std::move(res) }; }; } + + if (context.initializeOnce("openmw_cellbindings")) + { + auto pathGridT = view.new_usertype("ESM3_PathGrid"); + pathGridT[sol::meta_function::to_string] = [](const ESM::Pathgrid& rec) -> std::string { + return "ESM3_PathGrid[" + rec.mCell.toDebugString() + "]"; + }; + pathGridT["getPoints"] = [](sol::this_state lua, const ESM::Pathgrid& rec) -> sol::table { + sol::table points(lua, sol::create); + for (const ESM::Pathgrid::Point& point : rec.mPoints) + { + sol::table table(lua, sol::create); + table["autoGenerated"] = point.mAutogenerated == 0; + table["relativePosition"] = osg::Vec3f(point.mX, point.mY, point.mZ); + sol::table edges(lua, sol::create); + table["connections"] = edges; + points.add(table); + } + for (const ESM::Pathgrid::Edge& edge : rec.mEdges) + { + sol::table p1 = points[edge.mV0 + 1]; + sol::table p2 = points[edge.mV1 + 1]; + p1.get("connections").add(p2); + p2.get("connections").add(p1); + } + return points; + }; + } } void initCellBindingsForLocalScripts(const Context& context) diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index b85c14578c..bdf71710af 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -27,6 +28,7 @@ #include "magicbindings.hpp" #include "soundbindings.hpp" #include "stats.hpp" +#include "weatherbindings.hpp" namespace MWLua { @@ -104,6 +106,9 @@ namespace MWLua api["land"] = context.cachePackage("openmw_core_land", [context]() { return initCoreLandBindings(context); }); + api["weather"] + = context.cachePackage("openmw_core_weather", [context]() { return initCoreWeatherBindings(context); }); + api["factions"] = context.cachePackage("openmw_core_factions", [context]() { return initCoreFactionBindings(context); }); api["dialogue"] @@ -155,6 +160,8 @@ namespace MWLua }; } + api["getGameDifficulty"] = []() { return Settings::game().mDifficulty.get(); }; + sol::table readOnlyApi = LuaUtil::makeReadOnly(api); return context.setTypePackage(readOnlyApi, "openmw_core"); } diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 6c652bccba..be7d249bfc 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -113,6 +113,15 @@ namespace MWLua scripts->onSkillLevelUp(event.mSkill, event.mSource); } + void operator()(const OnJailTimeServed& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onJailTimeServed(event.mDays); + } + private: MWWorld::Ptr getPtr(ESM::RefNum id) const { diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index fb9183eb7c..407739d60e 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -70,8 +70,13 @@ namespace MWLua std::string mSkill; std::string mSource; }; + struct OnJailTimeServed + { + ESM::RefNum mActor; + int mDays; + }; using Event = std::variant; + OnAnimationTextKey, OnSkillUse, OnSkillLevelUp, OnJailTimeServed>; void clear() { mQueue.clear(); } void addToQueue(Event e) { mQueue.push_back(std::move(e)); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 4bdfb0d13a..ad2913dd49 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -232,16 +232,19 @@ namespace MWLua [&](LuaUtil::LuaView& view) { addPackage("openmw.self", sol::make_object(view.sol(), &mData)); }); registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers, &mOnSkillUse, - &mOnSkillLevelUp }); + &mOnSkillLevelUp, &mOnJailTimeServed }); } - void LocalScripts::setActive(bool active) + void LocalScripts::setActive(bool active, bool callHandlers) { mData.mIsActive = active; - if (active) - callEngineHandlers(mOnActiveHandlers); - else - callEngineHandlers(mOnInactiveHandlers); + if (callHandlers) + { + if (active) + callEngineHandlers(mOnActiveHandlers); + else + callEngineHandlers(mOnInactiveHandlers); + } } void LocalScripts::applyStatsCache() diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index adbf20292d..bc34576509 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -10,6 +10,7 @@ #include #include "../mwbase/luamanager.hpp" +#include "../mwmechanics/actorutil.hpp" #include "object.hpp" @@ -67,7 +68,7 @@ namespace MWLua MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } const MWWorld::Ptr& getPtrOrEmpty() const { return mData.ptrOrEmpty(); } - void setActive(bool active); + void setActive(bool active, bool callHandlers = true); bool isActive() const override { return mData.mIsActive; } void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); } void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); } @@ -88,9 +89,23 @@ namespace MWLua { callEngineHandlers(mOnSkillLevelUp, skillId, source); } + void onJailTimeServed(int days) { callEngineHandlers(mOnJailTimeServed, days); } void applyStatsCache(); + // Calls a lua interface on the player's scripts. This call is only meant for use in updating UI elements. + template + static std::optional callPlayerInterface( + std::string_view interfaceName, std::string_view identifier, const Args&... args) + { + auto player = MWMechanics::getPlayer(); + auto scripts = player.getRefData().getLuaScripts(); + if (scripts) + return scripts->callInterface(interfaceName, identifier, args...); + + return std::nullopt; + } + protected: SelfObject mData; @@ -104,6 +119,7 @@ namespace MWLua EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; EngineHandlerList mOnSkillUse{ "_onSkillUse" }; EngineHandlerList mOnSkillLevelUp{ "_onSkillLevelUp" }; + EngineHandlerList mOnJailTimeServed{ "_onJailTimeServed" }; }; } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 5fa2d9867c..850dd87eed 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -5,8 +5,6 @@ #include #include -#include "sol/state_view.hpp" - #include #include @@ -152,6 +150,17 @@ namespace MWLua }); } + void LuaManager::sendLocalEvent( + const MWWorld::Ptr& target, const std::string& name, const std::optional& data) + { + LuaUtil::BinaryData binary = {}; + if (data) + { + binary = LuaUtil::serialize(*data, mLocalSerializer.get()); + } + mLuaEvents.addLocalEvent({ getId(target), name, binary }); + } + void LuaManager::update() { if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0) @@ -203,13 +212,12 @@ namespace MWLua // Run engine handlers mEngineEvents.callEngineHandlers(); - if (!timeManager.isPaused()) - { - float frameDuration = MWBase::Environment::get().getFrameDuration(); - for (LocalScripts* scripts : mActiveLocalScripts) - scripts->update(frameDuration); - mGlobalScripts.update(frameDuration); - } + bool isPaused = timeManager.isPaused(); + + float frameDuration = MWBase::Environment::get().getFrameDuration(); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(isPaused ? 0 : frameDuration); + mGlobalScripts.update(isPaused ? 0 : frameDuration); mLua.protectedCall([&](LuaUtil::LuaView& lua) { mScriptTracker.unloadInactiveScripts(lua); }); } @@ -482,6 +490,54 @@ namespace MWLua EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) }); } + void LuaManager::jailTimeServed(const MWWorld::Ptr& actor, int days) + { + mEngineEvents.addToQueue(EngineEvents::OnJailTimeServed{ getId(actor), days }); + } + + void LuaManager::onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) + { + mLua.protectedCall([&](LuaUtil::LuaView& view) { + sol::table damageTable = view.newTable(); + if (isHealth) + damageTable["health"] = damage; + else + damageTable["fatigue"] = damage; + + sol::table data = view.newTable(); + if (!attacker.isEmpty()) + data["attacker"] = LObject(attacker); + if (!weapon.isEmpty()) + data["weapon"] = LObject(weapon); + if (!ammo.isEmpty()) + data["ammo"] = LObject(weapon); + data["type"] = attackType; + data["strength"] = attackStrength; + data["damage"] = damageTable; + data["hitPos"] = hitPos; + data["successful"] = successful; + switch (sourceType) + { + case MWMechanics::DamageSourceType::Unspecified: + data["sourceType"] = "unspecified"; + break; + case MWMechanics::DamageSourceType::Melee: + data["sourceType"] = "melee"; + break; + case MWMechanics::DamageSourceType::Ranged: + data["sourceType"] = "ranged"; + break; + case MWMechanics::DamageSourceType::Magical: + data["sourceType"] = "magic"; + break; + } + + sendLocalEvent(victim, "Hit", data); + }); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. @@ -540,7 +596,10 @@ namespace MWLua localScripts = createLocalScripts(ptr); localScripts->addAutoStartedScripts(); if (ptr.isInCell() && MWBase::Environment::get().getWorldScene()->isCellActive(*ptr.getCell())) + { + localScripts->setActive(true, false); mActiveLocalScripts.insert(localScripts); + } } localScripts->addCustomScript(scriptId, initData); } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 3f2135e9c9..bd4ab0d30e 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -43,7 +43,7 @@ namespace MWLua void init(); void loadPermanentStorage(const std::filesystem::path& userConfigPath); - void savePermanentStorage(const std::filesystem::path& userConfigPath); + void savePermanentStorage(const std::filesystem::path& userConfigPath) override; // \brief Executes lua handlers. Defaults to running in parallel with OSG Cull. // @@ -92,6 +92,10 @@ namespace MWLua bool loopfallback) override; void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override; void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override; + void jailTimeServed(const MWWorld::Ptr& actor, int days) override; + void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon, + const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth, + const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); @@ -166,6 +170,9 @@ namespace MWLua LuaUtil::InputAction::Registry& inputActions() { return mInputActions; } LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; } + void sendLocalEvent( + const MWWorld::Ptr& target, const std::string& name, const std::optional& data = std::nullopt); + private: void initConfiguration(); LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr, diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 19dada74a7..d2afde9536 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -976,9 +976,6 @@ namespace MWLua bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; - bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce; - if (!appliedOnce) - effect.mDuration = std::max(1.f, effect.mDuration); effect.mTimeLeft = effect.mDuration; params.getEffects().emplace_back(effect); diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index a6d762499a..6c244a0fd4 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -233,6 +233,7 @@ namespace MWLua DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::AreaCosts areaCosts{}; float destinationTolerance = 1; + std::vector checkpoints; if (options.has_value()) { @@ -258,13 +259,24 @@ namespace MWLua } if (const auto& v = options->get>("destinationTolerance")) destinationTolerance = *v; + if (const auto& t = options->get>("checkpoints")) + { + for (const auto& [k, v] : *t) + { + const int index = k.as(); + const osg::Vec3f position = v.as(); + if (index != static_cast(checkpoints.size() + 1)) + throw std::runtime_error("checkpoints is not an array"); + checkpoints.push_back(position); + } + } } std::vector path; - const DetourNavigator::Status status - = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, - source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); + const DetourNavigator::Status status = DetourNavigator::findPath( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, + includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path)); sol::table result(lua, sol::create); LuaUtil::copyVectorToTable(path, result); diff --git a/apps/openmw/mwlua/postprocessingbindings.cpp b/apps/openmw/mwlua/postprocessingbindings.cpp index 6127cb4b27..f12bda8650 100644 --- a/apps/openmw/mwlua/postprocessingbindings.cpp +++ b/apps/openmw/mwlua/postprocessingbindings.cpp @@ -35,9 +35,9 @@ namespace MWLua { struct Shader { - std::shared_ptr mShader; + std::shared_ptr mShader; - Shader(std::shared_ptr shader) + Shader(std::shared_ptr shader) : mShader(std::move(shader)) { } diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index 413a656e90..1629235fdd 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -420,6 +420,42 @@ namespace MWLua return ptr.getClass().getCapacity(ptr); }; + actor["_onHit"] = [context](const SelfObject& self, const sol::table& options) { + sol::optional damageLua = options.get>("damage"); + std::map damageCpp; + if (damageLua) + { + for (auto& [key, value] : damageLua.value()) + { + damageCpp[key.as()] = value.as(); + } + } + std::string sourceTypeStr = options.get_or("sourceType", "unspecified"); + MWMechanics::DamageSourceType sourceType = MWMechanics::DamageSourceType::Unspecified; + if (sourceTypeStr == "melee") + sourceType = MWMechanics::DamageSourceType::Melee; + else if (sourceTypeStr == "ranged") + sourceType = MWMechanics::DamageSourceType::Ranged; + else if (sourceTypeStr == "magic") + sourceType = MWMechanics::DamageSourceType::Magical; + sol::optional weapon = options.get>("weapon"); + sol::optional ammo = options.get>("ammo"); + + context.mLuaManager->addAction( + [self = self, damages = std::move(damageCpp), attacker = options.get>("attacker"), + weapon = ammo ? ammo : weapon, successful = options.get("successful"), + sourceType = sourceType] { + MWWorld::Ptr attackerPtr; + MWWorld::Ptr weaponPtr; + if (attacker) + attackerPtr = attacker->ptr(); + if (weapon) + weaponPtr = weapon->ptr(); + self.ptr().getClass().onHit(self.ptr(), damages, weaponPtr, attackerPtr, successful, sourceType); + }, + "HitAction"); + }; + addActorStatsBindings(actor, context); addActorMagicBindings(actor, context); } diff --git a/apps/openmw/mwlua/types/creature.cpp b/apps/openmw/mwlua/types/creature.cpp index 4ebc658eb9..a9a1be9eee 100644 --- a/apps/openmw/mwlua/types/creature.cpp +++ b/apps/openmw/mwlua/types/creature.cpp @@ -74,6 +74,7 @@ namespace MWLua [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; }); record["isRespawning"] = sol::readonly_property( [](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; }); + record["bloodType"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mBloodType; }); addActorServicesBindings(record, context); } diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index 58a53a7124..5db6f9b875 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -149,5 +149,9 @@ namespace MWLua addModelProperty(record); record["isAutomatic"] = sol::readonly_property( [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); + record["openSound"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mOpenSound).serializeText(); }); + record["closeSound"] = sol::readonly_property( + [](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mCloseSound).serializeText(); }); } } diff --git a/apps/openmw/mwlua/types/npc.cpp b/apps/openmw/mwlua/types/npc.cpp index e649c56a0f..380a2d1e9b 100644 --- a/apps/openmw/mwlua/types/npc.cpp +++ b/apps/openmw/mwlua/types/npc.cpp @@ -102,6 +102,7 @@ namespace MWLua record["isRespawning"] = sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; }); record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; }); + record["bloodType"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mBloodType; }); addActorServicesBindings(record, context); npc["classes"] = initClassRecordBindings(context); diff --git a/apps/openmw/mwlua/types/player.cpp b/apps/openmw/mwlua/types/player.cpp index 15dc719f2e..e8e0eaebb5 100644 --- a/apps/openmw/mwlua/types/player.cpp +++ b/apps/openmw/mwlua/types/player.cpp @@ -6,6 +6,7 @@ #include "../birthsignbindings.hpp" #include "../luamanagerimp.hpp" +#include "apps/openmw/mwbase/dialoguemanager.hpp" #include "apps/openmw/mwbase/inputmanager.hpp" #include "apps/openmw/mwbase/journal.hpp" #include "apps/openmw/mwbase/mechanicsmanager.hpp" @@ -195,6 +196,22 @@ namespace MWLua throw std::runtime_error("Only player and global scripts can toggle teleportation."); MWBase::Environment::get().getWorld()->enableTeleporting(state); }; + player["addTopic"] = [](const Object& player, std::string_view topicId) { + verifyPlayer(player); + + ESM::RefId topic = ESM::RefId::deserializeText(topicId); + const ESM::Dialogue* dialogueRecord + = MWBase::Environment::get().getESMStore()->get().search(topic); + + if (!dialogueRecord) + throw std::runtime_error( + "Failed to add topic \"" + std::string(topicId) + "\": topic record not found"); + + if (dialogueRecord->mType != ESM::Dialogue::Topic) + throw std::runtime_error("Failed to add topic \"" + std::string(topicId) + "\": record is not a topic"); + + MWBase::Environment::get().getDialogueManager()->addTopic(topic); + }; player["sendMenuEvent"] = [context](const Object& player, std::string eventName, const sol::object& eventData) { verifyPlayer(player); context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) }); diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index bc5581eb74..8d9892005c 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -108,6 +108,10 @@ namespace MWLua } luaManager->addUIMessage(message, mode); }; + + api["_showInteractiveMessage"] = [windowManager](std::string_view message, sol::optional) { + windowManager->interactiveMessageBox(message, { "#{Interface:OK}" }); + }; api["CONSOLE_COLOR"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(lua, { { "Default", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Default.substr(1)) }, @@ -296,6 +300,8 @@ namespace MWLua luaManager->addAction( [=, window = std::move(window)]() { windowManager->setDisabledByLua(window, disabled); }); }; + api["_isWindowVisible"] + = [windowManager](std::string_view window) { return windowManager->isWindowVisible(window); }; // TODO // api["_showMouseCursor"] = [](bool) {}; diff --git a/apps/openmw/mwlua/weatherbindings.cpp b/apps/openmw/mwlua/weatherbindings.cpp new file mode 100644 index 0000000000..daabd08620 --- /dev/null +++ b/apps/openmw/mwlua/weatherbindings.cpp @@ -0,0 +1,185 @@ +#include "weatherbindings.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/weather.hpp" + +#include "context.hpp" + +namespace +{ + class WeatherStore + { + public: + const MWWorld::Weather* get(size_t index) const + { + return MWBase::Environment::get().getWorld()->getWeather(index); + } + const MWWorld::Weather* get(const ESM::RefId& id) const + { + return MWBase::Environment::get().getWorld()->getWeather(id); + } + size_t size() const { return MWBase::Environment::get().getWorld()->getAllWeather().size(); } + }; + + Misc::Color color(const osg::Vec4f& color) + { + return Misc::Color(color.r(), color.g(), color.b(), color.a()); + } +} + +namespace MWLua +{ + sol::table initCoreWeatherBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + auto weatherT = lua.new_usertype("Weather"); + weatherT[sol::meta_function::to_string] + = [](const MWWorld::Weather& w) -> std::string { return "Weather[" + w.mName + "]"; }; + weatherT["name"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mName; }); + weatherT["windSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mWindSpeed; }); + weatherT["cloudSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mCloudSpeed; }); + weatherT["cloudTexture"] = sol::readonly_property([vfs](const MWWorld::Weather& w) { + return Misc::ResourceHelpers::correctTexturePath(w.mCloudTexture, vfs); + }); + weatherT["cloudsMaximumPercent"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mCloudsMaximumPercent; }); + weatherT["isStorm"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mIsStorm; }); + weatherT["stormDirection"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mStormDirection; }); + weatherT["glareView"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mGlareView; }); + weatherT["rainSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainSpeed; }); + weatherT["rainEntranceSpeed"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainEntranceSpeed; }); + weatherT["rainEffect"] = sol::readonly_property([](const MWWorld::Weather& w) -> sol::optional { + if (w.mRainEffect.empty()) + return sol::nullopt; + return w.mRainEffect; + }); + weatherT["rainMaxRaindrops"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMaxRaindrops; }); + weatherT["rainDiameter"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainDiameter; }); + weatherT["rainThreshold"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainThreshold; }); + weatherT["rainMaxHeight"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMaxHeight; }); + weatherT["rainMinHeight"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMinHeight; }); + weatherT["rainLoopSoundID"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainLoopSoundID.serializeText(); }); + weatherT["thunderSoundID"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + for (const auto& soundId : w.mThunderSoundID) + result.add(soundId.serializeText()); + return result; + }); + weatherT["sunDiscSunsetColor"] + = sol::readonly_property([](const MWWorld::Weather& w) { return color(w.mSunDiscSunsetColor); }); + weatherT["ambientLoopSoundID"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mAmbientLoopSoundID.serializeText(); }); + weatherT["ambientColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + result["sunrise"] = color(w.mAmbientColor.getSunriseValue()); + result["day"] = color(w.mAmbientColor.getDayValue()); + result["sunset"] = color(w.mAmbientColor.getSunsetValue()); + result["night"] = color(w.mAmbientColor.getNightValue()); + return result; + }); + weatherT["fogColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + result["sunrise"] = color(w.mFogColor.getSunriseValue()); + result["day"] = color(w.mFogColor.getDayValue()); + result["sunset"] = color(w.mFogColor.getSunsetValue()); + result["night"] = color(w.mFogColor.getNightValue()); + return result; + }); + weatherT["skyColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + result["sunrise"] = color(w.mSkyColor.getSunriseValue()); + result["day"] = color(w.mSkyColor.getDayValue()); + result["sunset"] = color(w.mSkyColor.getSunsetValue()); + result["night"] = color(w.mSkyColor.getNightValue()); + return result; + }); + weatherT["sunColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + result["sunrise"] = color(w.mSunColor.getSunriseValue()); + result["day"] = color(w.mSunColor.getDayValue()); + result["sunset"] = color(w.mSunColor.getSunsetValue()); + result["night"] = color(w.mSunColor.getNightValue()); + return result; + }); + weatherT["landFogDepth"] = sol::readonly_property([lua](const MWWorld::Weather& w) { + sol::table result(lua, sol::create); + result["sunrise"] = w.mLandFogDepth.getSunriseValue(); + result["day"] = w.mLandFogDepth.getDayValue(); + result["sunset"] = w.mLandFogDepth.getSunsetValue(); + result["night"] = w.mLandFogDepth.getNightValue(); + return result; + }); + weatherT["particleEffect"] + = sol::readonly_property([](const MWWorld::Weather& w) -> sol::optional { + if (w.mParticleEffect.empty()) + return sol::nullopt; + return w.mParticleEffect; + }); + weatherT["distantLandFogFactor"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mDL.FogFactor; }); + weatherT["distantLandFogOffset"] + = sol::readonly_property([](const MWWorld::Weather& w) { return w.mDL.FogOffset; }); + weatherT["scriptId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mScriptId; }); + weatherT["recordId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mId.serializeText(); }); + + api["getCurrent"] = []() { return MWBase::Environment::get().getWorld()->getCurrentWeather(); }; + api["getNext"] + = []() -> const MWWorld::Weather* { return MWBase::Environment::get().getWorld()->getNextWeather(); }; + api["getTransition"] = []() { return MWBase::Environment::get().getWorld()->getWeatherTransition(); }; + + api["changeWeather"] = [](std::string_view regionId, const MWWorld::Weather& weather) { + ESM::RefId region = ESM::RefId::deserializeText(regionId); + MWBase::Environment::get().getESMStore()->get().find(region); + MWBase::Environment::get().getWorld()->changeWeather(region, weather.mId); + }; + + sol::usertype storeT = lua.new_usertype("WeatherWorldStore"); + storeT[sol::meta_function::to_string] + = [](const WeatherStore& store) { return "{" + std::to_string(store.size()) + " Weather records}"; }; + storeT[sol::meta_function::length] = [](const WeatherStore& store) { return store.size(); }; + storeT[sol::meta_function::index] = sol::overload( + [](const WeatherStore& store, size_t index) -> const MWWorld::Weather* { + return store.get(LuaUtil::fromLuaIndex(index)); + }, + [](const WeatherStore& store, std::string_view id) -> const MWWorld::Weather* { + return store.get(ESM::RefId::deserializeText(id)); + }); + storeT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + storeT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + + // Provide access to the store. + api["records"] = WeatherStore{}; + + api["getCurrentSunLightDirection"] = []() { + osg::Vec4f sunPos = MWBase::Environment::get().getWorld()->getSunLightPosition(); + // normalize to get the direction towards the sun + sunPos.normalize(); + + // and invert it to get the direction of the sun light + return -sunPos; + }; + api["getCurrentSunVisibility"] = []() { return MWBase::Environment::get().getWorld()->getSunVisibility(); }; + api["getCurrentSunPercentage"] = []() { return MWBase::Environment::get().getWorld()->getSunPercentage(); }; + api["getCurrentWindSpeed"] = []() { return MWBase::Environment::get().getWorld()->getWindSpeed(); }; + api["getCurrentStormDirection"] = []() { return MWBase::Environment::get().getWorld()->getStormDirection(); }; + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/weatherbindings.hpp b/apps/openmw/mwlua/weatherbindings.hpp new file mode 100644 index 0000000000..c4686602ff --- /dev/null +++ b/apps/openmw/mwlua/weatherbindings.hpp @@ -0,0 +1,14 @@ +#ifndef MWLUA_WEATHERBINDINGS_H +#define MWLUA_WEATHERBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initCoreWeatherBindings(const Context&); + +} + +#endif // MWLUA_WEATHERBINDINGS_H diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 0f361f7ffc..7a4369a464 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -73,6 +73,21 @@ namespace namespace MWMechanics { + struct ActiveSpells::UpdateContext + { + bool mUpdatedEnemy = false; + bool mUpdatedHitOverlay = false; + bool mUpdateSpellWindow = false; + bool mPlayNonLooping = false; + bool mEraseRemoved = false; + bool mUpdate; + + UpdateContext(bool update) + : mUpdate(update) + { + } + }; + ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { @@ -256,8 +271,9 @@ namespace MWMechanics ++spellIt; } + UpdateContext context(duration > 0.f); for (const auto& spell : mQueue) - addToSpells(ptr, spell); + addToSpells(ptr, spell, context); mQueue.clear(); // Vanilla only does this on cell change I think @@ -267,19 +283,17 @@ namespace MWMechanics if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) { - mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true }); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, ActiveSpellParams{ spell, ptr, true }, context); } } - bool updateSpellWindow = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { auto& store = ptr.getClass().getInventoryStore(ptr); if (store.getInvListener() != nullptr) { - bool playNonLooping = !store.isFirstEquip(); + context.mPlayNonLooping = !store.isFirstEquip(); const auto world = MWBase::Environment::get().getWorld(); for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) { @@ -305,85 +319,108 @@ namespace MWMechanics // invisibility manually purgeEffect(ptr, ESM::MagicEffect::Invisibility); applyPurges(ptr); - ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); - params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); - for (const auto& effect : params.mEffects) - MWMechanics::playEffects( - ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); - updateSpellWindow = true; + ActiveSpellParams* params = initParams(ptr, ActiveSpellParams{ *slot, enchantment, ptr }, context); + if (params) + context.mUpdateSpellWindow = true; } } } const MWWorld::Ptr player = MWMechanics::getPlayer(); - bool updatedHitOverlay = false; - bool updatedEnemy = false; // Update effects + context.mEraseRemoved = true; for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { - const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( - spellIt->mCasterActorId); // Maybe make this search outside active grid? - bool removedSpell = false; - std::optional reflected; - for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) - { - auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); - if (result.mType == MagicApplicationResult::Type::REFLECTED) - { - if (!reflected) - { - if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) - reflected = { *spellIt, caster }; - else - reflected = { *spellIt, ptr }; - } - auto& reflectedEffect = reflected->mEffects.emplace_back(*it); - reflectedEffect.mFlags - = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; - it = spellIt->mEffects.erase(it); - } - else if (result.mType == MagicApplicationResult::Type::REMOVED) - it = spellIt->mEffects.erase(it); - else - { - ++it; - if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player) - { - MWBase::Environment::get().getWindowManager()->setEnemy(ptr); - updatedEnemy = true; - } - if (!updatedHitOverlay && result.mShowHit && ptr == player) - { - MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); - updatedHitOverlay = true; - } - } - removedSpell = applyPurges(ptr, &spellIt, &it); - if (removedSpell) - break; - } - if (reflected) - { - const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( - ESM::RefId::stringRefId("VFX_Reflect")); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); - if (animation && !reflectStatic->mModel.empty()) - { - const VFS::Path::Normalized reflectStaticModel - = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); - animation->addEffect( - reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); - } - caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); - } - if (removedSpell) - continue; + updateActiveSpell(ptr, duration, spellIt, context); + } + if (Settings::game().mClassicCalmSpellsBehavior) + { + ESM::MagicEffect::Effects effect + = ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature; + if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f) + creatureStats.getAiSequence().stopCombat(); + } + + if (ptr == player && context.mUpdateSpellWindow) + { + // Something happened with the spell list -- possibly while the game is paused, + // so we want to make the spell window get the memo. + // We don't normally want to do this, so this targets constant enchantments. + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); + } + } + + bool ActiveSpells::updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context) + { + const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( + spellIt->mCasterActorId); // Maybe make this search outside active grid? + bool removedSpell = false; + std::optional reflected; + for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) + { + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, context.mPlayNonLooping); + if (result.mType == MagicApplicationResult::Type::REFLECTED) + { + if (!reflected) + { + if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) + reflected = { *spellIt, caster }; + else + reflected = { *spellIt, ptr }; + } + auto& reflectedEffect = reflected->mEffects.emplace_back(*it); + reflectedEffect.mFlags + = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + it = spellIt->mEffects.erase(it); + } + else if (result.mType == MagicApplicationResult::Type::REMOVED) + it = spellIt->mEffects.erase(it); + else + { + const MWWorld::Ptr player = MWMechanics::getPlayer(); + ++it; + if (!context.mUpdatedEnemy && result.mShowHealth && caster == player && ptr != player) + { + MWBase::Environment::get().getWindowManager()->setEnemy(ptr); + context.mUpdatedEnemy = true; + } + if (!context.mUpdatedHitOverlay && result.mShowHit && ptr == player) + { + MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); + context.mUpdatedHitOverlay = true; + } + } + removedSpell = applyPurges(ptr, &spellIt, &it); + if (removedSpell) + break; + } + if (reflected) + { + const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( + ESM::RefId::stringRefId("VFX_Reflect")); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); + if (animation && !reflectStatic->mModel.empty()) + { + const VFS::Path::Normalized reflectStaticModel + = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); + animation->addEffect( + reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); + } + caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); + } + if (removedSpell) + return true; + + if (context.mEraseRemoved) + { bool remove = false; if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) { try { + auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells(); remove = !spells.hasSpell(spellIt->mSourceSpellId); } catch (const std::runtime_error& e) @@ -415,30 +452,27 @@ namespace MWMechanics for (const auto& effect : params.mEffects) onMagicEffectRemoved(ptr, params, effect); applyPurges(ptr, &spellIt); - updateSpellWindow = true; - continue; + context.mUpdateSpellWindow = true; + return true; } - ++spellIt; - } - - if (Settings::game().mClassicCalmSpellsBehavior) - { - ESM::MagicEffect::Effects effect - = ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature; - if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f) - creatureStats.getAiSequence().stopCombat(); - } - - if (ptr == player && updateSpellWindow) - { - // Something happened with the spell list -- possibly while the game is paused, - // so we want to make the spell window get the memo. - // We don't normally want to do this, so this targets constant enchantments. - MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } + ++spellIt; + return false; } - void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) + ActiveSpells::ActiveSpellParams* ActiveSpells::initParams( + const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context) + { + mSpells.emplace_back(params).setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + auto it = mSpells.end(); + --it; + // We instantly apply the effect with a duration of 0 so continuous effects can be purged before truly applying + if (context.mUpdate && updateActiveSpell(ptr, 0.f, it, context)) + return nullptr; + return &*it; + } + + void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context) { if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { @@ -456,8 +490,7 @@ namespace MWMechanics onMagicEffectRemoved(ptr, params, effect); } } - mSpells.emplace_back(spell); - mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + initParams(ptr, spell, context); } ActiveSpells::ActiveSpells() @@ -610,6 +643,8 @@ namespace MWMechanics { purge( [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + return false; if (effectArg.empty()) return effect.mEffectId == effectId; return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 3e4dafdb26..465e5aa456 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -116,17 +116,23 @@ namespace MWMechanics IterationGuard(ActiveSpells& spells); ~IterationGuard(); }; + struct UpdateContext; std::list mSpells; std::vector mQueue; std::queue mPurges; bool mIterating; - void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); + void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context); bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); + bool updateActiveSpell( + const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context); + + ActiveSpellParams* initParams(const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context); + public: ActiveSpells(); diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index d7438712d9..45c077f98c 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -28,7 +28,7 @@ namespace MWMechanics class Actor { public: - Actor(const MWWorld::Ptr& ptr, MWRender::Animation* animation) + Actor(const MWWorld::Ptr& ptr, MWRender::Animation& animation) : mCharacterController(ptr, animation) , mPositionAdjusted(ptr.getClass().getCreatureStats(ptr).getFallHeight() > 0) { @@ -62,14 +62,22 @@ namespace MWMechanics void setPositionAdjusted(bool adjusted) { mPositionAdjusted = adjusted; } bool getPositionAdjusted() const { return mPositionAdjusted; } + void invalidate() + { + mInvalid = true; + mCharacterController.detachAnimation(); + } + bool isInvalid() const { return mInvalid; } + private: CharacterController mCharacterController; int mGreetingTimer{ 0 }; float mTargetAngleRadians{ 0.f }; - GreetingState mGreetingState{ Greet_None }; - bool mIsTurningToPlayer{ false }; + GreetingState mGreetingState{ GreetingState::None }; Misc::DeviatingPeriodicTimer mEngageCombat{ 1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng()) }; + bool mIsTurningToPlayer{ false }; + bool mInvalid{ false }; bool mPositionAdjusted; }; diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 1e62cc4a21..4766afb55a 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -50,6 +50,7 @@ #include "attacktype.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "steering.hpp" @@ -122,6 +123,8 @@ namespace { for (const MWMechanics::Actor& actor : actors) { + if (actor.isInvalid()) + continue; const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == player || iteratedActor == actorPtr) continue; @@ -345,7 +348,7 @@ namespace MWMechanics // Find something nearby. for (const Actor& otherActor : actors) { - if (otherActor.getPtr() == ptr) + if (otherActor.isInvalid() || otherActor.getPtr() == ptr) continue; updateHeadTracking( @@ -485,7 +488,7 @@ namespace MWMechanics { actorState.setTurningToPlayer(false); actorState.setGreetingTimer(0); - actorState.setGreetingState(Greet_None); + actorState.setGreetingState(GreetingState::None); return; } @@ -523,7 +526,7 @@ namespace MWMechanics int greetingTimer = actorState.getGreetingTimer(); GreetingState greetingState = actorState.getGreetingState(); - if (greetingState == Greet_None) + if (greetingState == GreetingState::None) { if ((playerPos - actorPos).length2() <= helloDistance * helloDistance && !playerStats.isDead() && !actorStats.isParalyzed() && !isTargetMagicallyHidden(player) @@ -533,14 +536,14 @@ namespace MWMechanics if (greetingTimer >= GREETING_SHOULD_START) { - greetingState = Greet_InProgress; + greetingState = GreetingState::InProgress; if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) - greetingState = Greet_Done; + greetingState = GreetingState::Done; greetingTimer = 0; } } - if (greetingState == Greet_InProgress) + if (greetingState == GreetingState::InProgress) { greetingTimer++; @@ -552,16 +555,16 @@ namespace MWMechanics if (greetingTimer >= GREETING_COOLDOWN) { - greetingState = Greet_Done; + greetingState = GreetingState::Done; greetingTimer = 0; } } - if (greetingState == Greet_Done) + if (greetingState == GreetingState::Done) { float resetDist = 2 * helloDistance; if ((playerPos - actorPos).length2() >= resetDist * resetDist) - greetingState = Greet_None; + greetingState = GreetingState::None; } actorState.setGreetingTimer(greetingTimer); @@ -605,10 +608,6 @@ namespace MWMechanics void Actors::engageCombat( const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, bool againstPlayer) const { - // No combat for totally static creatures - if (!actor1.getClass().isMobile(actor1)) - return; - CreatureStats& creatureStats1 = actor1.getClass().getCreatureStats(actor1); if (creatureStats1.isDead() || creatureStats1.getAiSequence().isInCombat(actor2)) return; @@ -685,7 +684,7 @@ namespace MWMechanics } } - if (creatureStats2.getMagicEffects().getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) + if (isTargetMagicallyHidden(actor2)) return; // Stop here if target is unreachable @@ -1066,9 +1065,12 @@ namespace MWMechanics { if (heldIter != inventoryStore.end() && heldIter->getType() == ESM::Light::sRecordId) { - // At day, unequip lights and auto equip shields or other suitable items - // (Note: autoEquip will ignore lights) - inventoryStore.autoEquip(); + // At day, unequip lights and auto equip shields + auto shield = inventoryStore.getPreferredShield(); + if (shield != inventoryStore.end()) + inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield); + else + inventoryStore.unequipSlot(MWWorld::InventoryStore::Slot_CarriedLeft); } } } @@ -1196,7 +1198,7 @@ namespace MWMechanics MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; - const auto it = mActors.emplace(mActors.end(), ptr, anim); + const auto it = mActors.emplace(mActors.end(), ptr, *anim); mIndex.emplace(ptr.mRef, it); if (updateImmediately) @@ -1248,7 +1250,7 @@ namespace MWMechanics { if (!keepActive) removeTemporaryEffects(iter->second->getPtr()); - mActors.erase(iter->second); + iter->second->invalidate(); mIndex.erase(iter); } } @@ -1300,16 +1302,15 @@ namespace MWMechanics void Actors::dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore) { - for (auto iter = mActors.begin(); iter != mActors.end();) + for (Actor& actor : mActors) { - if ((iter->getPtr().isInCell() && iter->getPtr().getCell() == cellStore) && iter->getPtr() != ignore) + if (!actor.isInvalid() && actor.getPtr().isInCell() && actor.getPtr().getCell() == cellStore + && actor.getPtr() != ignore) { - removeTemporaryEffects(iter->getPtr()); - mIndex.erase(iter->getPtr().mRef); - iter = mActors.erase(iter); + removeTemporaryEffects(actor.getPtr()); + mIndex.erase(actor.getPtr().mRef); + actor.invalidate(); } - else - ++iter; } } @@ -1326,17 +1327,37 @@ namespace MWMechanics const MWWorld::Ptr player = getPlayer(); const MWBase::World* const world = MWBase::Environment::get().getWorld(); + + struct CacheEntry + { + MWWorld::Ptr mPtr; + float mMaxSpeed; + osg::Vec3f mHalfExtents; + Movement& mMovement; + }; + + std::vector cache; + cache.reserve(mActors.size()); for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const MWWorld::Ptr& ptr = actor.getPtr(); + const MWWorld::Class& cls = ptr.getClass(); + cache.push_back({ ptr, cls.getMaxSpeed(ptr), world->getHalfExtents(ptr), cls.getMovementSettings(ptr) }); + } + + for (const CacheEntry& cached : cache) + { + const MWWorld::Ptr& ptr = cached.mPtr; if (ptr == player) continue; // Don't interfere with player controls. - const float maxSpeed = ptr.getClass().getMaxSpeed(ptr); + const float maxSpeed = cached.mMaxSpeed; if (maxSpeed == 0.0) continue; // Can't move, so there is no sense to predict collisions. - Movement& movement = ptr.getClass().getMovementSettings(ptr); + Movement& movement = cached.mMovement; const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); const bool isMoving = origMovement.length2() > 0.01; if (movement.mPosition[1] < 0) @@ -1377,7 +1398,7 @@ namespace MWMechanics const osg::Vec2f baseSpeed = origMovement * maxSpeed; const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); const float baseRotZ = ptr.getRefData().getPosition().rot[2]; - const osg::Vec3f halfExtents = world->getHalfExtents(ptr); + const osg::Vec3f& halfExtents = cached.mHalfExtents; const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; float timeToCheck = maxTimeToCheck; @@ -1390,13 +1411,13 @@ namespace MWMechanics float angleToApproachingActor = 0; // Iterate through all other actors and predict collisions. - for (const Actor& otherActor : mActors) + for (const CacheEntry& otherCached : cache) { - const MWWorld::Ptr& otherPtr = otherActor.getPtr(); + const MWWorld::Ptr& otherPtr = otherCached.mPtr; if (otherPtr == ptr || otherPtr == currentTarget) continue; - const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + const osg::Vec3f& otherHalfExtents = otherCached.mHalfExtents; const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); const float dist = deltaPos.length(); @@ -1409,8 +1430,7 @@ namespace MWMechanics if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; - const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() - * otherPtr.getClass().getMaxSpeed(otherPtr); + const osg::Vec3f speed = otherCached.mMovement.asVec3() * otherCached.mMaxSpeed; const float rotZ = otherPtr.getRefData().getPosition().rot[2]; const osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; @@ -1510,6 +1530,8 @@ namespace MWMechanics // AI and magic effects update for (Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const bool isPlayer = actor.getPtr() == player; CharacterController& ctrl = actor.getCharacterController(); MWBase::LuaManager::ActorControls* luaControls @@ -1571,6 +1593,8 @@ namespace MWMechanics for (const Actor& otherActor : mActors) { + if (otherActor.isInvalid()) + continue; if (otherActor.getPtr() == actor.getPtr() || isPlayer) // player is not AI-controlled continue; engageCombat( @@ -1628,6 +1652,8 @@ namespace MWMechanics CharacterController* playerCharacter = nullptr; for (Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const float dist = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length(); const bool isPlayer = actor.getPtr() == player; CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); @@ -1693,8 +1719,15 @@ namespace MWMechanics luaControls->mJump = false; } - for (const Actor& actor : mActors) + for (auto it = mActors.begin(); it != mActors.end();) { + if (it->isInvalid()) + { + it = mActors.erase(it); + continue; + } + const Actor& actor = *it; + it++; const MWWorld::Class& cls = actor.getPtr().getClass(); CreatureStats& stats = cls.getCreatureStats(actor.getPtr()); @@ -1744,6 +1777,8 @@ namespace MWMechanics { for (Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const MWWorld::Class& cls = actor.getPtr().getClass(); CreatureStats& stats = cls.getCreatureStats(actor.getPtr()); @@ -1831,6 +1866,8 @@ namespace MWMechanics { for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; MWMechanics::ActiveSpells& spells = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActiveSpells(); spells.purge(actor.getPtr(), casterActorId); @@ -1850,6 +1887,8 @@ namespace MWMechanics for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { adjustMagicEffects(actor.getPtr(), duration); @@ -2047,7 +2086,10 @@ namespace MWMechanics void Actors::persistAnimationStates() const { for (const Actor& actor : mActors) - actor.getCharacterController().persistAnimationState(); + { + if (!actor.isInvalid()) + actor.getCharacterController().persistAnimationState(); + } } void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) @@ -2061,6 +2103,8 @@ namespace MWMechanics { for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) out.push_back(actor.getPtr()); } @@ -2070,6 +2114,8 @@ namespace MWMechanics { for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) return true; } @@ -2083,6 +2129,8 @@ namespace MWMechanics list.push_back(actorPtr); for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == getPlayer()) continue; @@ -2334,7 +2382,7 @@ namespace MWMechanics { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) - return Greet_None; + return GreetingState::None; return it->second->getGreetingState(); } @@ -2353,10 +2401,11 @@ namespace MWMechanics if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; - for (auto it = mActors.begin(); it != mActors.end();) + for (const Actor& actor : mActors) { - const MWWorld::Ptr ptr = it->getPtr(); - ++it; + if (actor.isInvalid()) + continue; + const MWWorld::Ptr ptr = actor.getPtr(); if (ptr == getPlayer() || !isConscious(ptr) || ptr.getClass().getCreatureStats(ptr).isParalyzed()) continue; MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 2399961a3a..cae4d57b39 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,13 +1,11 @@ #include "aicombat.hpp" -#include -#include - -#include - -#include - #include +#include +#include +#include +#include +#include #include #include "../mwphysics/raycasting.hpp" @@ -104,10 +102,10 @@ namespace MWMechanics bool AiCombat::execute( const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { - // get or create temporary storage + // Get or create temporary storage AiCombatStorage& storage = state.get(); - // General description + // No combat for dead creatures if (actor.getClass().getCreatureStats(actor).isDead()) return true; @@ -124,6 +122,13 @@ namespace MWMechanics if (actor == target) // This should never happen. return true; + // No actions for totally static creatures + if (!actor.getClass().isMobile(actor)) + { + storage.mFleeState = AiCombatStorage::FleeState_Idle; + return false; + } + if (!storage.isFleeing()) { if (storage.mCurrentAction.get()) // need to wait to init action with its attack range @@ -171,11 +176,16 @@ namespace MWMechanics currentCell = actor.getCell(); } + const MWWorld::Class& actorClass = actor.getClass(); + MWMechanics::CreatureStats& stats = actorClass.getCreatureStats(actor); + if (stats.isParalyzed() || stats.getKnockedDown()) + return false; + bool forceFlee = false; if (!canFight(actor, target)) { storage.stopAttack(); - actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + stats.setAttackingOrSpell(false); storage.mActionCooldown = 0.f; // Continue combat if target is player or player follower/escorter and an attack has been attempted const auto& playerFollowersAndEscorters @@ -184,18 +194,14 @@ namespace MWMechanics = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end()); if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) - && ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() - == target.getClass().getCreatureStats(target).getActorId()) - || (target.getClass().getCreatureStats(target).getHitAttemptActorId() - == actor.getClass().getCreatureStats(actor).getActorId()))) + && ((stats.getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId()) + || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == stats.getActorId()))) forceFlee = true; else // Otherwise end combat return true; } - const MWWorld::Class& actorClass = actor.getClass(); - actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); - + stats.setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; std::unique_ptr& currentAction = storage.mCurrentAction; @@ -295,8 +301,8 @@ namespace MWMechanics const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { @@ -309,8 +315,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, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. @@ -323,7 +329,7 @@ namespace MWMechanics { storage.mUseCustomDestination = false; storage.stopAttack(); - actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); + stats.setAttackingOrSpell(false); currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); storage.startFleeing(); @@ -386,13 +392,13 @@ namespace MWMechanics osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); - int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); - for (int i = 0; i < static_cast(pathgrid->mPoints.size()); i++) + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos); + for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++) { if (i != closestPointIndex && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) { - points.push_back(pathgrid->mPoints[static_cast(i)]); + points.push_back(pathgrid->mPoints[i]); } } @@ -448,7 +454,8 @@ namespace MWMechanics float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) - || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) + || pathTo( + actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections)) { state = AiCombatStorage::FleeState_Idle; } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index d5a9c3464c..42baaf6349 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -2,12 +2,13 @@ #define GAME_MWMECHANICS_AICOMBAT_H #include "aitemporarybase.hpp" +#include "aitimer.hpp" +#include "movement.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors -#include "aitimer.hpp" -#include "movement.hpp" +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bcfc7dedd..3fcb28307c 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& = world->getStore().get().search(*actor.getCell()->getCell()); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); + mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) - result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; + { + result |= DetourNavigator::Flag_walk; + if (getTypeId() != AiPackageTypeId::Wander) + result |= DetourNavigator::Flag_usePathgrid; + } if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index edb62c97c4..42aa62ffe3 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -16,6 +16,8 @@ namespace ESM { struct Cell; + struct Pathgrid; + namespace AiSequence { struct AiSequence; diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index f0781565bf..a669de8339 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -12,6 +12,7 @@ #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" namespace @@ -77,7 +78,7 @@ namespace MWMechanics if (!stats.getMovementFlag(CreatureStats::Flag_ForceJump) && !stats.getMovementFlag(CreatureStats::Flag_ForceSneak) - && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == Greet_InProgress)) + && (mechMgr->isTurningToPlayer(actor) || mechMgr->getGreetingState(actor) == GreetingState::InProgress)) return false; const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 464d83ad46..ad4fc730df 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -24,22 +25,12 @@ #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" +#include "greetingstate.hpp" #include "movement.hpp" #include "pathgrid.hpp" namespace MWMechanics { - static const int COUNT_BEFORE_RESET = 10; - static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; - - // to prevent overcrowding - static const int DESTINATION_TOLERANCE = 64; - - // distance must be long enough that NPC will need to move to get there. - static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; - - static const std::size_t MAX_IDLE_SIZE = 8; - const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { "idle2", "idle3", @@ -53,11 +44,22 @@ namespace MWMechanics namespace { + constexpr int countBeforeReset = 10; + constexpr float idlePositionCheckInterval = 1.5f; + + // to prevent overcrowding + constexpr unsigned destinationTolerance = 64; + + // distance must be long enough that NPC will need to move to get there. + constexpr unsigned minimumWanderDistance = destinationTolerance * 2; + + constexpr std::size_t maxIdleSize = 8; + inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; - return COUNT_BEFORE_RESET; + return countBeforeReset; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) @@ -99,16 +101,42 @@ namespace MWMechanics std::vector getInitialIdle(const std::vector& idle) { - std::vector result(MAX_IDLE_SIZE, 0); - std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); + std::vector result(maxIdleSize, 0); + std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin()); return result; } - std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) + std::vector getInitialIdle(const unsigned char (&idle)[maxIdleSize]) { return std::vector(std::begin(idle), std::end(idle)); } + void trimAllowedPositions(const std::deque& path, std::vector& allowedPositions) + { + // TODO: how to add these back in once the door opens? + // Idea: keep a list of detected closed doors (see aicombat.cpp) + // Every now and then check whether one of the doors is opened. (maybe + // at the end of playing idle?) If the door is opened then re-calculate + // allowed positions starting from the spawn point. + std::vector points(path.begin(), path.end()); + while (points.size() >= 2) + { + const osg::Vec3f point = points.back(); + for (std::size_t j = 0; j < allowedPositions.size(); j++) + { + // FIXME: doesn't handle a door with the same X/Y + // coordinates but with a different Z + if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5 + && std::abs(allowedPositions[j].y() - point.y()) <= 0.5) + { + allowedPositions.erase(allowedPositions.begin() + j); + break; + } + } + points.pop_back(); + } + } + } AiWanderStorage::AiWanderStorage() @@ -118,9 +146,9 @@ namespace MWMechanics , mCanWanderAlongPathGrid(true) , mIdleAnimation(0) , mBadIdles() - , mPopulateAvailableNodes(true) - , mAllowedNodes() - , mTrimCurrentNode(false) + , mPopulateAvailablePositions(true) + , mAllowedPositions() + , mTrimCurrentPosition(false) , mCheckIdlePositionTimer(0) , mStuckCount(0) { @@ -128,8 +156,8 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat) : TypedAiPackage(repeat) - , mDistance(std::max(0, distance)) - , mDuration(std::max(0, duration)) + , mDistance(static_cast(std::max(0, distance))) + , mDuration(static_cast(std::max(0, duration))) , mRemainingDuration(duration) , mTimeOfDay(timeOfDay) , mIdle(getInitialIdle(idle)) @@ -215,20 +243,12 @@ namespace MWMechanics { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*actor.getCell()->getCell()); - if (mUsePathgrid) - { - mPathFinder.buildPathByPathgrid( - pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); - } - else - { - const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); - constexpr float endTolerance = 0; - const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); - const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); - } + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); @@ -238,7 +258,7 @@ namespace MWMechanics && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); - if (greetingState == Greet_InProgress) + if (greetingState == GreetingState::InProgress) { if (storage.mState == AiWanderStorage::Wander_Walking) { @@ -259,9 +279,6 @@ namespace MWMechanics bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (mDistance <= 0) - storage.mCanWanderAlongPathGrid = false; - if (isPackageCompleted()) { stopWalking(actor); @@ -276,13 +293,15 @@ namespace MWMechanics mStoredInitialActorPosition = true; } - // Initialization to discover & store allowed node points for this actor. - if (storage.mPopulateAvailableNodes) + // Initialization to discover & store allowed positions points for this actor. + if (storage.mPopulateAvailablePositions) { - getAllowedNodes(actor, storage); + fillAllowedPositions(actor, storage); } - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + + auto& prng = world.getPrng(); if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander @@ -295,7 +314,7 @@ namespace MWMechanics } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point - else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100, prng) >= 96) @@ -307,7 +326,7 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_IdleNow); } } - else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } @@ -323,9 +342,9 @@ namespace MWMechanics // Construct a new path if there isn't one if (!mPathFinder.isPathConstructed()) { - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setPathToAnAllowedNode(actor, storage, pos); + setPathToAnAllowedPosition(actor, storage, pos); } } } @@ -336,7 +355,7 @@ namespace MWMechanics if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) - || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) + || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed @@ -366,12 +385,12 @@ namespace MWMechanics std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const auto world = MWBase::Environment::get().getWorld(); - const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigator = world->getNavigator(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + const auto agentBounds = world.getPathfindingAgentBounds(actor); + const auto navigator = world.getNavigator(); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + Misc::Rng::Generator& prng = world.getPrng(); do { @@ -411,7 +430,7 @@ namespace MWMechanics if (isDestinationHidden(actor, mDestination)) continue; - if (isAreaOccupiedByOtherActor(actor, mDestination)) + if (world.isAreaOccupiedByOtherActor(actor, mDestination)) continue; constexpr float endTolerance = 0; @@ -491,24 +510,24 @@ namespace MWMechanics void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { - // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. + // Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; - if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) + if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; - if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) + if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); - storage.mTrimCurrentNode = false; // just in case + storage.mTrimCurrentPosition = false; // just in case return; } } // Check if idle animation finished GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); - if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) + if (!checkIdle(actor, storage.mIdleAnimation) && greetingState != GreetingState::InProgress) { if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); @@ -517,16 +536,14 @@ namespace MWMechanics } } - bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const + bool AiWander::isNearAllowedPosition( + const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) - { - osg::Vec3f point(node.mX, node.mY, node.mZ); - if ((actorPos - point).length2() < distance * distance) - return true; - } - return false; + const float squaredDistance = distance * distance; + return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) { + return (actorPos - v).length2() < squaredDistance; + }) != storage.mAllowedPositions.end(); } void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, @@ -535,7 +552,7 @@ namespace MWMechanics // Is there no destination or are we there yet? if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, - DESTINATION_TOLERANCE)) + destinationTolerance)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -586,8 +603,8 @@ namespace MWMechanics if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination - storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, mPathFinder); + storage.mTrimCurrentPosition = true; + trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -606,67 +623,67 @@ namespace MWMechanics } } - void AiWander::setPathToAnAllowedNode( + void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - auto world = MWBase::Environment::get().getWorld(); - auto& prng = world->getPrng(); - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; + MWBase::World& world = *MWBase::Environment::get().getWorld(); + Misc::Rng::Generator& prng = world.getPrng(); + const std::size_t randomAllowedPositionIndex + = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); + const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); - // don't take shortcuts for wandering - const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); - mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); + const MWWorld::Cell& cell = *actor.getCell()->getCell(); + const ESM::Pathgrid* pathgrid = world.getStore().get().search(cell); + const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid); - if (mPathFinder.isPathConstructed()) + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell); + std::deque path + = pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)), + Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition))); + + // Choose a different position and delete this one from possible positions because it is uncreachable: + if (path.empty()) { - mDestination = destVec3f; - mHasDestination = true; - mUsePathgrid = true; - // Remove this node as an option and add back the previously used node (stops NPC from picking the same - // node): - ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - // check if mCurrentNode was taken out of mAllowedNodes - if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) - storage.mTrimCurrentNode = false; - else - storage.mAllowedNodes.push_back(storage.mCurrentNode); - storage.mCurrentNode = temp; - - storage.setState(AiWanderStorage::Wander_Walking); + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; } - // Choose a different node and delete this one from possible nodes because it is uncreachable: + + // Drop nearest pathgrid point. + path.pop_front(); + + std::vector checkpoints(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i])); + + const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor); + const DetourNavigator::Flags flags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags); + constexpr float endTolerance = 0; + mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, PathType::Full, checkpoints); + + if (!mPathFinder.isPathConstructed()) + { + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + mDestination = randomAllowedPosition; + mHasDestination = true; + mUsePathgrid = true; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; else - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - } + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; - void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) - { - // TODO: how to add these back in once the door opens? - // Idea: keep a list of detected closed doors (see aicombat.cpp) - // Every now and then check whether one of the doors is opened. (maybe - // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. - auto paths = pathfinder.getPath(); - while (paths.size() >= 2) - { - const auto pt = paths.back(); - for (unsigned int j = 0; j < nodes.size(); j++) - { - // FIXME: doesn't handle a door with the same X/Y - // coordinates but with a different Z - if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) - { - nodes.erase(nodes.begin() + j); - break; - } - } - paths.pop_back(); - } + storage.setState(AiWanderStorage::Wander_Walking); } void AiWander::stopWalking(const MWWorld::Ptr& actor) @@ -742,20 +759,20 @@ namespace MWMechanics return; AiWanderStorage& storage = state.get(); - if (storage.mPopulateAvailableNodes) - getAllowedNodes(actor, storage); + if (storage.mPopulateAvailablePositions) + fillAllowedPositions(actor, storage); - if (storage.mAllowedNodes.empty()) + if (storage.mAllowedPositions.empty()) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; + int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng); + const osg::Vec3f worldDest = storage.mAllowedPositions[index]; const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); - ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); + osg::Vec3f dest = converter.toLocalVec3(worldDest); - bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + const bool isPathGridOccupied + = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) @@ -775,19 +792,17 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); - float length = dir.length(); + osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest; + const float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node - dest - = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); - worldDest = converter.toWorldPoint(dest); + dest = dest + dir * (j * 5 * length / 100.f); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + converter.toWorldVec3(dest), 60); if (!isOccupied) break; @@ -807,19 +822,18 @@ namespace MWMechanics // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // underground. Adding 20 in adjustPosition() is not enough. - dest.mZ += 60; + dest.z() += 60; converter.toWorld(dest); state.reset(); - osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); - MWBase::Environment::get().getWorld()->moveObject(actor, pos); + MWBase::Environment::get().getWorld()->moveObject(actor, dest); actor.getClass().adjustPosition(actor, false); } void AiWander::getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*currentCell->getCell()); @@ -827,19 +841,19 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + const size_t index = Misc::getClosestPoint(*pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } - void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) + void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const MWWorld::CellStore* cellStore = actor.getCell(); const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); - storage.mAllowedNodes.clear(); + storage.mAllowedPositions.clear(); // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 @@ -860,34 +874,35 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos); - // mAllowedNodes for this actor with pathgrid point indexes based on mDistance + // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point // NOTE: mPoints is in local coordinates size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); + const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { - storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); + storage.mAllowedPositions.push_back( + Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter]))); pointIndex = counter; } } - if (storage.mAllowedNodes.size() == 1) + if (storage.mAllowedPositions.size() == 1) { - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); + storage.mAllowedPositions.push_back(mInitialActorPosition); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setCurrentNodeToClosestAllowedNode(storage); + setCurrentPositionToClosestAllowedPosition(storage); } } - storage.mPopulateAvailableNodes = false; + storage.mPopulateAvailablePositions = false; } // When only one path grid point in wander distance, @@ -901,44 +916,44 @@ namespace MWMechanics { if (edge.mV0 == pointIndex) { - AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), + addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); } } } - void AiWander::AddPointBetweenPathGridPoints( + void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { - osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); - osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; + osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start); + osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart; float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); + unsigned distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint - distance = std::min(distance, static_cast(length)); + distance = std::min(distance, static_cast(length)); delta *= distance; - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); + storage.mAllowedPositions.push_back(vectorStart + delta); } - void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) + void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage) { - float distanceToClosestNode = std::numeric_limits::max(); + float distanceToClosestPosition = std::numeric_limits::max(); size_t index = 0; - for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) + for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); - float tempDist = (mInitialActorPosition - nodePos).length2(); - if (tempDist < distanceToClosestNode) + const osg::Vec3f position = storage.mAllowedPositions[i]; + const float tempDist = (mInitialActorPosition - position).length2(); + if (tempDist < distanceToClosestPosition) { index = i; - distanceToClosestNode = tempDist; + distanceToClosestPosition = tempDist; } } - storage.mCurrentNode = storage.mAllowedNodes[index]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); + storage.mCurrentPosition = storage.mAllowedPositions[index]; + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const @@ -970,8 +985,8 @@ namespace MWMechanics AiWander::AiWander(const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) - , mDistance(std::max(static_cast(0), wander->mData.mDistance)) - , mDuration(std::max(static_cast(0), wander->mData.mDuration)) + , mDistance(static_cast(std::max(static_cast(0), wander->mData.mDistance))) + , mDuration(static_cast(std::max(static_cast(0), wander->mData.mDuration))) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f08980ad29..3e0b704524 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H -#include "typedaipackage.hpp" - -#include -#include - #include "aitemporarybase.hpp" #include "aitimer.hpp" #include "pathfinding.hpp" +#include "typedaipackage.hpp" + +#include + +#include +#include namespace ESM { @@ -51,14 +52,13 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors - // do we need to calculate allowed nodes based on mDistance - bool mPopulateAvailableNodes; + bool mPopulateAvailablePositions; - // allowed pathgrid nodes based on mDistance from the spawn point - std::vector mAllowedNodes; + // allowed destination positions based on mDistance from the spawn point + std::vector mAllowedPositions; - ESM::Pathgrid::Point mCurrentNode; - bool mTrimCurrentNode; + osg::Vec3f mCurrentPosition; + bool mTrimCurrentPosition; float mCheckIdlePositionTimer; int mStuckCount; @@ -132,7 +132,8 @@ namespace MWMechanics bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); int getRandomIdle() const; - void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); + void setPathToAnAllowedPosition( + const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); @@ -145,28 +146,27 @@ namespace MWMechanics void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); - bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; + bool isNearAllowedPosition(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const int mDistance; // how far the actor can wander from the spawn point - const int mDuration; + // how far the actor can wander from the spawn point + const unsigned mDistance; + const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; bool mStoredInitialActorPosition; - osg::Vec3f - mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell + // Note: an original engine does not reset coordinates even when actor changes a cell + osg::Vec3f mInitialActorPosition; bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); - void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); - - void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); + void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage); // constants for converting idleSelect values into groupNames enum GroupIndex @@ -175,12 +175,12 @@ namespace MWMechanics GroupIndex_MaxIdle = 9 }; - void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); + void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage); void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, const Misc::CoordinateConverter& converter); - void AddPointBetweenPathGridPoints( + void addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 87cc469eb4..3f4f6c6956 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -535,7 +535,7 @@ namespace MWMechanics bool CharacterController::onOpen() const { - if (mPtr.getType() == ESM::Container::sRecordId) + if (mPtr.getType() == ESM::Container::sRecordId && mAnimation) { if (!mAnimation->hasAnimation("containeropen")) return true; @@ -559,7 +559,7 @@ namespace MWMechanics { if (mPtr.getType() == ESM::Container::sRecordId) { - if (!mAnimation->hasAnimation("containerclose")) + if (!mAnimation || !mAnimation->hasAnimation("containerclose")) return; float complete, startPoint = 0.f; @@ -886,11 +886,12 @@ namespace MWMechanics if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr)) mDeathState = CharState_SwimDeath; - if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState))) + if (mDeathState == CharState_None + || (mAnimation && !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState)))) mDeathState = chooseRandomDeathState(); // Do not interrupt scripted animation by death - if (isScriptedAnimPlaying()) + if (!mAnimation || isScriptedAnimPlaying()) return; playDeath(startpoint, mDeathState); @@ -910,13 +911,10 @@ namespace MWMechanics return result; } - CharacterController::CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim) + CharacterController::CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation& anim) : mPtr(ptr) - , mAnimation(anim) + , mAnimation(&anim) { - if (!mAnimation) - return; - mAnimation->setTextKeyListener(this); const MWWorld::Class& cls = mPtr.getClass(); @@ -992,17 +990,25 @@ namespace MWMechanics } CharacterController::~CharacterController() + { + detachAnimation(); + } + + void CharacterController::detachAnimation() { if (mAnimation) { persistAnimationState(); mAnimation->setTextKeyListener(nullptr); + mAnimation = nullptr; } } void CharacterController::handleTextKey( std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { + if (!mAnimation) + return; std::string_view evt = key->second; MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); @@ -1232,7 +1238,8 @@ namespace MWMechanics float CharacterController::calculateWindUp() const { - if (mCurrentWeapon.empty() || mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon)) + if (!mAnimation || mCurrentWeapon.empty() || mWeaponType == ESM::Weapon::PickProbe + || isRandomAttackAnimation(mCurrentWeapon)) return -1.f; float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack"); @@ -1950,6 +1957,8 @@ namespace MWMechanics void CharacterController::update(float duration) { + if (!mAnimation) + return; MWBase::World* world = MWBase::Environment::get().getWorld(); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); const MWWorld::Class& cls = mPtr.getClass(); @@ -2301,17 +2310,13 @@ namespace MWMechanics } else { - // Do not play turning animation for player if rotation speed is very slow. - // Actual threshold should take framerate in account. - float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; - // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. if (!sneak && !isFirstPersonPlayer && isBiped) { - if (effectiveRotation > rotationThreshold) + if (effectiveRotation > 0.f) movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; - else if (effectiveRotation < -rotationThreshold) + else if (effectiveRotation < 0.f) movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; } } @@ -2337,34 +2342,19 @@ namespace MWMechanics vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef); } - // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering - if (isPlayer) + if (isBiped) { - float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; - float complete; - bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); - if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) - { - if (animPlaying && complete < threshold) - movestate = mMovementState; - } - } - else - { - if (isBiped) - { - if (mTurnAnimationThreshold > 0) - mTurnAnimationThreshold -= duration; + if (mTurnAnimationThreshold > 0) + mTurnAnimationThreshold -= duration; - if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft - || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) - { - mTurnAnimationThreshold = 0.05f; - } - else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) - { - movestate = mMovementState; - } + if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft + || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) + { + mTurnAnimationThreshold = 0.05f; + } + else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; } } @@ -2393,11 +2383,10 @@ namespace MWMechanics if (isTurning()) { - // Adjust animation speed from 1.0 to 1.5 multiplier if (duration > 0) { float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI)); - mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); + mAnimation->adjustSpeedMult(mCurrentMovement, turnSpeed); } } else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) @@ -2528,7 +2517,7 @@ namespace MWMechanics ESM::AnimationState::ScriptedAnimation anim; anim.mGroup = iter->mGroup; - if (iter == mAnimQueue.begin()) + if (iter == mAnimQueue.begin() && mAnimation) { float complete; size_t loopcount; @@ -2741,23 +2730,18 @@ namespace MWMechanics void CharacterController::clearAnimQueue(bool clearScriptedAnims) { // Do not interrupt scripted animations, if we want to keep them - if ((!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty()) + if (mAnimation && (!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); if (clearScriptedAnims) { - mAnimation->setPlayScriptedOnly(false); + if (mAnimation) + mAnimation->setPlayScriptedOnly(false); mAnimQueue.clear(); return; } - for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) - { - if (!it->mScripted) - it = mAnimQueue.erase(it); - else - ++it; - } + std::erase_if(mAnimQueue, [](const AnimationQueueEntry& entry) { return !entry.mScripted; }); } void CharacterController::forceStateUpdate() @@ -2866,6 +2850,8 @@ namespace MWMechanics void CharacterController::setVisibility(float visibility) const { + if (!mAnimation) + return; // We should take actor's invisibility in account if (mPtr.getClass().isActor()) { @@ -2926,7 +2912,7 @@ namespace MWMechanics bool CharacterController::isReadyToBlock() const { - return updateCarriedLeftVisible(mWeaponType); + return mAnimation && updateCarriedLeftVisible(mWeaponType); } bool CharacterController::isKnockedDown() const @@ -3030,7 +3016,8 @@ namespace MWMechanics void CharacterController::setActive(int active) const { - mAnimation->setActive(active); + if (mAnimation) + mAnimation->setActive(active); } void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr& target) @@ -3061,6 +3048,8 @@ namespace MWMechanics float CharacterController::getAnimationMovementDirection() const { + if (!mAnimation) + return 0.f; switch (mMovementState) { case CharState_RunLeft: @@ -3155,6 +3144,8 @@ namespace MWMechanics MWWorld::MovementDirectionFlags CharacterController::getSupportedMovementDirections() const { + if (!mAnimation) + return 0; using namespace std::string_view_literals; // There are fallbacks in the CharacterController::refreshMovementAnims for certain animations. Arrays below // represent them. diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index d5c642c883..2a1982c664 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -252,13 +252,21 @@ namespace MWMechanics void prepareHit(); + void unpersistAnimationState(); + + void playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, + bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, + uint32_t loops, bool loopfallback = false) const; + public: - CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); + CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation& anim); virtual ~CharacterController(); CharacterController(const CharacterController&) = delete; CharacterController(CharacterController&&) = delete; + void detachAnimation(); + const MWWorld::Ptr& getPtr() const { return mPtr; } void handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, @@ -275,11 +283,6 @@ namespace MWMechanics void onClose() const; void persistAnimationState() const; - void unpersistAnimationState(); - - void playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, - bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, - uint32_t loops, bool loopfallback = false) const; bool playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted = false); bool playGroupLua(std::string_view groupname, float speed, std::string_view startKey, std::string_view stopKey, uint32_t loops, bool forceLoop); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index e7c7342284..7c0c674986 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -12,6 +12,7 @@ #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -240,8 +241,8 @@ namespace MWMechanics if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, - MWMechanics::DamageSourceType::Ranged); + MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, + attackStrength, damage, false, hitPosition, false, MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } @@ -299,8 +300,8 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1); } - victim.getClass().onHit( - victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged); + MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, attackStrength, + damage, true, hitPosition, true, MWMechanics::DamageSourceType::Ranged); } } diff --git a/apps/openmw/mwmechanics/greetingstate.hpp b/apps/openmw/mwmechanics/greetingstate.hpp index 9b37096322..4a5a4aa2f8 100644 --- a/apps/openmw/mwmechanics/greetingstate.hpp +++ b/apps/openmw/mwmechanics/greetingstate.hpp @@ -3,11 +3,11 @@ namespace MWMechanics { - enum GreetingState + enum class GreetingState { - Greet_None, - Greet_InProgress, - Greet_Done + None, + InProgress, + Done }; } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 384c25953b..a3e260a4a7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -368,17 +368,28 @@ namespace MWMechanics bool MechanicsManager::isRunning(const MWWorld::Ptr& ptr) { - return mActors.isRunning(ptr); + CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + if (!stats.getStance(MWMechanics::CreatureStats::Stance_Run)) + return false; + + if (mActors.isRunning(ptr)) + return true; + + MWBase::World* world = MWBase::Environment::get().getWorld(); + return !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); } bool MechanicsManager::isSneaking(const MWWorld::Ptr& ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); + if (!stats.getStance(MWMechanics::CreatureStats::Stance_Sneak)) + return false; + + if (mActors.isSneaking(ptr)) + return true; + MWBase::World* world = MWBase::Environment::get().getWorld(); - bool animActive = mActors.isSneaking(ptr); - bool stanceOn = stats.getStance(MWMechanics::CreatureStats::Stance_Sneak); - bool inair = !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); - return stanceOn && (animActive || inair); + return !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); } void MechanicsManager::rest(double hours, bool sleep) @@ -1442,6 +1453,7 @@ namespace MWMechanics } startCombat(actor, player, &playerFollowers); + observerStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off @@ -1725,6 +1737,8 @@ namespace MWMechanics .getActorId()); // Stops guard from ending combat if player is unreachable for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; if (actor.getPtr().getClass().isClass(actor.getPtr(), "Guard")) { MWMechanics::AiSequence& aiSeq diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index 12d342666b..62f0df556d 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -20,7 +20,7 @@ namespace MWMechanics if (anim == nullptr) return; - const auto it = mObjects.emplace(mObjects.end(), ptr, anim); + const auto it = mObjects.emplace(mObjects.end(), ptr, *anim); mIndex.emplace(ptr.mRef, it); } diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index a6eb4f9c24..9a66eafb51 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,21 +106,6 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, - std::vector* occupyingActors) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - if (ignorePlayer) - { - const std::array ignore{ actor, world->getPlayerConstPtr() }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } - const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } - ObstacleCheck::ObstacleCheck() : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 532bc91331..a1c973765f 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -5,8 +5,6 @@ #include -#include - namespace MWWorld { class Ptr; @@ -24,9 +22,6 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, - bool ignorePlayer = false, std::vector* occupyingActors = nullptr); - class ObstacleCheck { public: diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 192cbdfe22..165250c5c8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -25,20 +26,20 @@ namespace { // Chooses a reachable end pathgrid point. start is assumed reachable. - std::pair getClosestReachablePoint( - const ESM::Pathgrid* grid, const MWMechanics::PathgridGraph* graph, const osg::Vec3f& pos, int start) + std::pair getClosestReachablePoint( + const ESM::Pathgrid* grid, const MWMechanics::PathgridGraph* graph, const osg::Vec3f& pos, size_t start) { assert(grid && !grid->mPoints.empty()); float closestDistanceBetween = std::numeric_limits::max(); float closestDistanceReachable = std::numeric_limits::max(); - int closestIndex = 0; - int closestReachableIndex = 0; + size_t closestIndex = 0; + size_t closestReachableIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { - float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); + float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one @@ -62,7 +63,7 @@ namespace // allowed nodes if not. Hence a path needs to be created even if the start // and the end points are the same. - return std::pair(closestReachableIndex, closestReachableIndex == closestIndex); + return { closestReachableIndex, closestReachableIndex == closestIndex }; } float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs) @@ -197,17 +198,17 @@ namespace MWMechanics // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); - int startNode = getClosestPoint(pathgrid, startPointInLocalCoords); + const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); - std::pair endNode + std::pair endNode = getClosestReachablePoint(pathgrid, &pathgridGraph, endPointInLocalCoords, startNode); // 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(); - float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); - float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); + float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); + float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; @@ -223,7 +224,7 @@ namespace MWMechanics { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); - *out++ = makeOsgVec3(temp); + *out++ = Misc::Convert::makeOsgVec3f(temp); } else { @@ -234,8 +235,8 @@ namespace MWMechanics if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); - osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); - osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); + osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]); + osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) @@ -259,7 +260,7 @@ namespace MWMechanics // convert supplied path to world coordinates std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { converter.toWorld(point); - return makeOsgVec3(point); + return Misc::Convert::makeOsgVec3f(point); }); } @@ -359,26 +360,16 @@ namespace MWMechanics mConstructed = true; } - void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) - { - mPath.clear(); - mCell = cell; - - buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - - mConstructed = !mPath.empty(); - } - void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::span checkpoints) { 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, agentBounds, flags, - areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -390,19 +381,19 @@ 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 DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::span checkpoints) { mPath.clear(); - mCell = cell; + mCell = actor.getCell(); DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, - pathType, std::back_inserter(mPath)); + pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } @@ -411,7 +402,7 @@ namespace MWMechanics && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -429,12 +420,13 @@ namespace MWMechanics DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out) + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out) { - const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); - const auto status = DetourNavigator::findPath( - *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const DetourNavigator::Navigator& navigator = *world.getNavigator(); + const DetourNavigator::Status status = DetourNavigator::findPath( + navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out); if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) return DetourNavigator::Status::Success; @@ -451,9 +443,9 @@ 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 DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance @@ -461,9 +453,9 @@ namespace MWMechanics const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, - endTolerance, pathType); + return buildPath( + actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); + buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 0f688686cd..55064d9e88 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -4,12 +4,13 @@ #include #include #include +#include + +#include #include #include #include -#include -#include namespace MWWorld { @@ -102,23 +103,20 @@ namespace MWMechanics void buildStraightPath(const osg::Vec3f& endPoint); - void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); - void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::span checkpoints = {}); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, @@ -145,61 +143,6 @@ namespace MWMechanics mPath.push_back(point); } - /// utility function to convert a osg::Vec3f to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) - { - return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); - } - - /// utility function to convert an ESM::Position to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) - { - return ESM::Pathgrid::Point( - static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); - } - - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) - { - return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); - } - - // Slightly cheaper version for comparisons. - // Caller needs to be careful for very short distances (i.e. less than 1) - // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 - // - static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) - { - return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); - } - - // Return the closest pathgrid point index from the specified position - // coordinates. NOTE: Does not check if there is a sensible way to get there - // (e.g. a cliff in front). - // - // NOTE: pos is expected to be in local coordinates, as is grid->mPoints - // - static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) - { - assert(grid && !grid->mPoints.empty()); - - float distanceBetween = distanceSquared(grid->mPoints[0], pos); - int closestIndex = 0; - - // TODO: if this full scan causes performance problems mapping pathgrid - // points to a quadtree may help - for (unsigned int counter = 1; counter < grid->mPoints.size(); counter++) - { - float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); - if (potentialDistBetween < distanceBetween) - { - distanceBetween = potentialDistBetween; - closestIndex = counter; - } - } - - return closestIndex; - } - private: bool mConstructed = false; std::deque mPath; @@ -211,7 +154,8 @@ namespace MWMechanics [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out); + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out); }; } diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 59e7e29a38..bf9d6aa025 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -215,10 +215,6 @@ namespace MWMechanics bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); effect.mDuration = hasDuration ? static_cast(enam.mData.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 diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 91e24f946f..efeb45dd86 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -359,8 +359,7 @@ namespace // Notify the target actor they've been hit bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful; if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful) - target.getClass().onHit( - target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical); + target.getClass().onHit(target, {}, MWWorld::Ptr(), caster, true, MWMechanics::DamageSourceType::Magical); // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { @@ -377,8 +376,11 @@ namespace MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); return MWMechanics::MagicApplicationResult::Type::REMOVED; } - effect.mMinMagnitude *= magnitudeMult; - effect.mMaxMagnitude *= magnitudeMult; + else if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + effect.mMinMagnitude *= magnitudeMult; + effect.mMaxMagnitude *= magnitudeMult; + } } return MWMechanics::MagicApplicationResult::Type::APPLIED; } @@ -519,8 +521,31 @@ namespace MWMechanics case ESM::MagicEffect::ExtraSpell: if (target.getClass().hasInventoryStore(target)) { - auto& store = target.getClass().getInventoryStore(target); - store.unequipAll(); + if (target != getPlayer()) + { + auto& store = target.getClass().getInventoryStore(target); + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + // Unequip everything except weapons, torches, and pants + switch (slot) + { + case MWWorld::InventoryStore::Slot_Ammunition: + case MWWorld::InventoryStore::Slot_CarriedRight: + case MWWorld::InventoryStore::Slot_Pants: + continue; + case MWWorld::InventoryStore::Slot_CarriedLeft: + { + auto carried = store.getSlot(slot); + if (carried == store.end() + || carried.getType() != MWWorld::ContainerStore::Type_Armor) + continue; + [[fallthrough]]; + } + default: + store.unequipSlot(slot); + } + } + } } else invalid = true; @@ -891,7 +916,7 @@ namespace MWMechanics } MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt) + ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt, bool playNonLooping) { const auto world = MWBase::Environment::get().getWorld(); bool invalid = false; @@ -988,11 +1013,13 @@ namespace MWMechanics else { // Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats - // updated instantly. We don't want to teleport instantly though + // updated instantly. We don't want to teleport instantly though. Nor do we want to force players to drink + // invisibility potions in the "right" order if (!dt && (effect.mEffectId == ESM::MagicEffect::Recall || effect.mEffectId == ESM::MagicEffect::DivineIntervention - || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention)) + || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention + || effect.mEffectId == ESM::MagicEffect::Invisibility)) return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); @@ -1009,9 +1036,12 @@ namespace MWMechanics oldMagnitude = effect.mMagnitude; else { - if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment) - && !spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua)) - playEffects(target, *magicEffect, spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)); + bool isTemporary = spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary); + bool isEquipment = spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment); + + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua)) + playEffects(target, *magicEffect, (isTemporary || (isEquipment && playNonLooping))); + if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() && target.getType() == ESM::Creature::sRecordId && target.get()->mBase->mData.mSoul == 0 && caster == getPlayer()) @@ -1083,7 +1113,7 @@ namespace MWMechanics } break; case ESM::MagicEffect::ExtraSpell: - if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f) + if (magnitudes.getOrDefault(effect.mEffectId).getMagnitude() <= 0.f && target != getPlayer()) target.getClass().getInventoryStore(target).autoEquip(); break; case ESM::MagicEffect::TurnUndead: diff --git a/apps/openmw/mwmechanics/spelleffects.hpp b/apps/openmw/mwmechanics/spelleffects.hpp index 2dafedf31f..d9b05535a9 100644 --- a/apps/openmw/mwmechanics/spelleffects.hpp +++ b/apps/openmw/mwmechanics/spelleffects.hpp @@ -28,7 +28,8 @@ namespace MWMechanics // Applies a tick of a single effect. Returns true if the effect should be removed immediately MagicApplicationResult applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, - ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt); + ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt, + bool playNonLoopingEffect = true); // Undoes permanent effects created by ESM::MagicEffect::AppliedOnce void onMagicEffectRemoved( diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index c1fa0ad1ee..6270cd3083 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection - bool testAabbAgainstSphere( + inline bool testAabbAgainstSphere( const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) { const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), @@ -18,35 +18,28 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, - const Ignore& ignore, OnCollision* onCollision) + explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, + const int group, const btCollisionObject* ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) , mCollisionFilterMask(mask) , mCollisionFilterGroup(group) - , mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult && mOnCollision == nullptr) + if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (mIgnore(collisionObject) || !needsCollision(*proxy) + if (mIgnore == collisionObject || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; - if (mOnCollision != nullptr) - { - (*mOnCollision)(collisionObject); - return true; - } return !mResult; } @@ -55,10 +48,9 @@ namespace MWPhysics private: btVector3 mPosition; btScalar mRadius; - Ignore mIgnore; + const btCollisionObject* mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; - OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5e7c70788d..f403f97c2f 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -849,36 +849,18 @@ namespace MWPhysics mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool PhysicsSystem::isAreaOccupiedByOtherActor( + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const { - std::vector ignoredObjects; - ignoredObjects.reserve(ignore.size()); - for (const auto& v : ignore) - if (const auto it = mActors.find(v.mRef); it != mActors.end()) - ignoredObjects.push_back(it->second->getCollisionObject()); - std::sort(ignoredObjects.begin(), ignoredObjects.end()); - ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); - const auto ignoreFilter = [&](const btCollisionObject* v) { - return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); - }; - const auto bulletPosition = Misc::Convert::toBullet(position); - const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); - const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); + const btCollisionObject* ignoredObject = nullptr; + if (const auto it = mActors.find(actor); it != mActors.end()) + ignoredObject = it->second->getCollisionObject(); + const btVector3 bulletPosition = Misc::Convert::toBullet(position); + const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius); + const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - if (occupyingActors == nullptr) - { - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, - static_cast(nullptr)); - mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); - return callback.getResult(); - } - const auto onCollision = [&](const btCollisionObject* object) { - if (PtrHolder* holder = static_cast(object->getUserPointer())) - occupyingActors->push_back(holder->getPtr()); - }; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 546d72676e..8a845b4c41 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -281,8 +281,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const; + bool isAreaOccupiedByOtherActor( + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index f07a325f7c..5912895855 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -722,6 +722,7 @@ namespace MWRender mAnimSources.push_back(animsrc); + mSupportedDirections.clear(); for (const std::string& group : mAnimSources.back()->getTextKeys().getGroups()) mSupportedAnimations.insert(group); @@ -795,6 +796,7 @@ namespace MWRender mAccumCtrl = nullptr; mSupportedAnimations.clear(); + mSupportedDirections.clear(); mAnimSources.clear(); mAnimVelocities.clear(); @@ -1115,8 +1117,8 @@ namespace MWRender return keyframeController->getAsCallback(); } - - return asCallback; + else + return asCallback; } void Animation::resetActiveGroups() @@ -2012,20 +2014,29 @@ namespace MWRender std::span prefixes) const { MWWorld::MovementDirectionFlags result = 0; - for (const std::string_view animation : mSupportedAnimations) + for (const std::string_view prefix : prefixes) { - if (std::find_if( - prefixes.begin(), prefixes.end(), [&](std::string_view v) { return animation.starts_with(v); }) - == prefixes.end()) - continue; - if (animation.ends_with("forward")) - result |= MWWorld::MovementDirectionFlag_Forward; - else if (animation.ends_with("back")) - result |= MWWorld::MovementDirectionFlag_Back; - else if (animation.ends_with("left")) - result |= MWWorld::MovementDirectionFlag_Left; - else if (animation.ends_with("right")) - result |= MWWorld::MovementDirectionFlag_Right; + auto it = std::find_if(mSupportedDirections.begin(), mSupportedDirections.end(), + [prefix](const auto& direction) { return direction.first == prefix; }); + if (it == mSupportedDirections.end()) + { + mSupportedDirections.emplace_back(prefix, 0); + it = mSupportedDirections.end() - 1; + for (const std::string_view animation : mSupportedAnimations) + { + if (!animation.starts_with(prefix)) + continue; + if (animation.ends_with("forward")) + it->second |= MWWorld::MovementDirectionFlag_Forward; + else if (animation.ends_with("back")) + it->second |= MWWorld::MovementDirectionFlag_Back; + else if (animation.ends_with("left")) + it->second |= MWWorld::MovementDirectionFlag_Left; + else if (animation.ends_with("right")) + it->second |= MWWorld::MovementDirectionFlag_Right; + } + } + result |= it->second; } return result; } @@ -2096,12 +2107,17 @@ namespace MWRender if (Settings::game().mGraphicHerbalism && ptr.getRefData().getCustomData() != nullptr && ObjectAnimation::canBeHarvested()) { - const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); - if (!store.hasVisibleItems()) - { - HarvestVisitor visitor; - mObjectRoot->accept(visitor); - } + harvest(ptr); + } + } + + void ObjectAnimation::harvest(const MWWorld::Ptr& ptr) + { + const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); + if (!store.hasVisibleItems()) + { + HarvestVisitor visitor; + mObjectRoot->accept(visitor); } } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index b6cb6f333c..807f785b1b 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -154,7 +154,7 @@ namespace MWRender float mLoopStopTime = 0; float mStopTime = 0; - std::shared_ptr mTime = std::make_shared(0); + std::shared_ptr mTime = std::make_shared(0.0f); float mSpeedMult = 1; bool mPlaying = false; @@ -181,6 +181,7 @@ namespace MWRender AnimSourceList mAnimSources; std::unordered_set mSupportedAnimations; + mutable std::vector> mSupportedDirections; osg::ref_ptr mInsert; @@ -483,6 +484,7 @@ namespace MWRender virtual void setAccurateAiming(bool enabled) {} virtual bool canBeHarvested() const { return false; } + virtual void harvest(const MWWorld::Ptr& ptr) {} virtual void removeFromScene(); @@ -498,6 +500,7 @@ namespace MWRender bool animated, bool allowLight); bool canBeHarvested() const override; + void harvest(const MWWorld::Ptr& ptr) override; }; class UpdateVfxCallback : public SceneUtil::NodeCallback diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index f45247398f..9f8b1ed86c 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -20,6 +20,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -36,12 +42,14 @@ #include "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" +#include "apps/openmw/mwclass/esm4base.hpp" #include "apps/openmw/mwworld/esmstore.hpp" #include "vismask.hpp" namespace MWRender { + namespace { bool typeFilter(int type, bool far) @@ -51,8 +59,14 @@ namespace MWRender case ESM::REC_STAT: case ESM::REC_ACTI: case ESM::REC_DOOR: + case ESM::REC_STAT4: + case ESM::REC_DOOR4: + case ESM::REC_TREE4: return true; case ESM::REC_CONT: + case ESM::REC_ACTI4: + case ESM::REC_CONT4: + case ESM::REC_FURN4: return !far; default: @@ -60,7 +74,16 @@ namespace MWRender } } - std::string getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) + template + std::string_view getEsm4Model(const Record& record) + { + if (MWClass::ESM4Impl::isMarkerModel(record->mModel)) + return {}; + else + return record->mModel; + } + + std::string_view getModel(int type, ESM::RefId id, const MWWorld::ESMStore& store) { switch (type) { @@ -72,6 +95,18 @@ namespace MWRender return store.get().searchStatic(id)->mModel; case ESM::REC_CONT: return store.get().searchStatic(id)->mModel; + case ESM::REC_STAT4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_DOOR4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_TREE4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_ACTI4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_CONT4: + return getEsm4Model(store.get().searchStatic(id)); + case ESM::REC_FURN4: + return getEsm4Model(store.get().searchStatic(id)); default: return {}; } @@ -494,6 +529,17 @@ namespace MWRender }; } + PagedCellRef makePagedCellRef(const ESM4::Reference& value) + { + return PagedCellRef{ + .mRefId = value.mBaseObj, + .mRefNum = value.mId, + .mPosition = value.mPos.asVec3(), + .mRotation = value.mPos.asRotationVec3(), + .mScale = value.mScale, + }; + } + std::map collectESM3References( float size, const osg::Vec2i& startCell, const MWWorld::ESMStore& store) { @@ -561,6 +607,45 @@ namespace MWRender } return refs; } + + std::map collectESM4References( + float size, const osg::Vec2i& startCell, ESM::RefId worldspace) + { + std::map refs; + const auto& store = MWBase::Environment::get().getWorld()->getStore(); + for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX) + { + for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY) + { + const ESM4::Cell* cell + = store.get().searchExterior(ESM::ExteriorCellLocation(cellX, cellY, worldspace)); + if (!cell) + continue; + for (const ESM4::Reference* ref4 : store.get().getByCell(cell->mId)) + { + if (ref4->mFlags & ESM4::Rec_Disabled) + continue; + int type = store.findStatic(ref4->mBaseObj); + if (!typeFilter(type, size >= 2)) + continue; + if (!ref4->mEsp.parent.isZeroOrUnset()) + { + const ESM4::Reference* parentRef + = store.get().searchStatic(ref4->mEsp.parent); + if (parentRef) + { + bool parentDisabled = parentRef->mFlags & ESM4::Rec_Disabled; + bool inversed = ref4->mEsp.flags & ESM4::EnableParent::Flag_Inversed; + if (parentDisabled != inversed) + continue; + } + } + refs.insert_or_assign(ref4->mId, makePagedCellRef(*ref4)); + } + } + } + return refs; + } } osg::ref_ptr ObjectPaging::createChunk(float size, const osg::Vec2f& center, bool activeGrid, @@ -578,7 +663,7 @@ namespace MWRender } else { - // TODO + refs = collectESM4References(size, startCell, mWorldspace); } if (activeGrid && !refs.empty()) @@ -648,12 +733,12 @@ namespace MWRender continue; const int type = store.findStatic(ref.mRefId); - VFS::Path::Normalized model = getModel(type, ref.mRefId, store); + VFS::Path::Normalized model(getModel(type, ref.mRefId, store)); if (model.empty()) continue; model = Misc::ResourceHelpers::correctMeshPath(model); - if (activeGrid && type != ESM::REC_STAT) + if (activeGrid && type != ESM::REC_STAT && type != ESM::REC_STAT4) { model = Misc::ResourceHelpers::correctActorModelPath(model, mSceneManager->getVFS()); if (Misc::getFileExtension(model) == "nif") diff --git a/apps/openmw/mwrender/pathgrid.cpp b/apps/openmw/mwrender/pathgrid.cpp index a39ba86a60..137a781342 100644 --- a/apps/openmw/mwrender/pathgrid.cpp +++ b/apps/openmw/mwrender/pathgrid.cpp @@ -58,8 +58,6 @@ namespace MWRender default: return false; } - - return false; } void Pathgrid::addCell(const MWWorld::CellStore* store) diff --git a/apps/openmw/mwrender/pingpongcanvas.cpp b/apps/openmw/mwrender/pingpongcanvas.cpp index 54d8145fa9..a289272d1b 100644 --- a/apps/openmw/mwrender/pingpongcanvas.cpp +++ b/apps/openmw/mwrender/pingpongcanvas.cpp @@ -46,7 +46,7 @@ namespace MWRender mMultiviewResolveStateSet->addUniform(new osg::Uniform("lastShader", 0)); } - void PingPongCanvas::setPasses(fx::DispatchArray&& passes) + void PingPongCanvas::setPasses(Fx::DispatchArray&& passes) { mPasses = std::move(passes); } @@ -54,8 +54,8 @@ namespace MWRender void PingPongCanvas::setMask(bool underwater, bool exterior) { mMask = 0; - mMask |= underwater ? fx::Technique::Flag_Disable_Underwater : fx::Technique::Flag_Disable_Abovewater; - mMask |= exterior ? fx::Technique::Flag_Disable_Exteriors : fx::Technique::Flag_Disable_Interiors; + mMask |= underwater ? Fx::Technique::Flag_Disable_Underwater : Fx::Technique::Flag_Disable_Abovewater; + mMask |= exterior ? Fx::Technique::Flag_Disable_Exteriors : Fx::Technique::Flag_Disable_Interiors; } void PingPongCanvas::drawGeometry(osg::RenderInfo& renderInfo) const @@ -280,15 +280,6 @@ namespace MWRender { pass.mRenderTarget->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER); - if (pass.mRenderTexture->getNumMipmapLevels() > 0) - { - state.setActiveTextureUnit(0); - state.applyTextureAttribute(0, - pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) - .getTexture()); - ext->glGenerateMipmap(GL_TEXTURE_2D); - } - lastApplied = pass.mRenderTarget->getHandle(state.getContextID()); } else if (pass.mResolve && index == filtered.back()) @@ -325,6 +316,15 @@ namespace MWRender drawGeometry(renderInfo); + if (pass.mRenderTarget && pass.mRenderTexture->getNumMipmapLevels() > 0) + { + state.setActiveTextureUnit(0); + state.applyTextureAttribute(0, + pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0) + .getTexture()); + ext->glGenerateMipmap(GL_TEXTURE_2D); + } + state.popStateSet(); state.apply(); } diff --git a/apps/openmw/mwrender/pingpongcanvas.hpp b/apps/openmw/mwrender/pingpongcanvas.hpp index 5a37b7fbc9..f5bfcb6ffe 100644 --- a/apps/openmw/mwrender/pingpongcanvas.hpp +++ b/apps/openmw/mwrender/pingpongcanvas.hpp @@ -31,14 +31,14 @@ namespace MWRender void dirty() { mDirty = true; } - void setDirtyAttachments(const std::vector& attachments) + void setDirtyAttachments(const std::vector& attachments) { mDirtyAttachments = attachments; } - const fx::DispatchArray& getPasses() { return mPasses; } + const Fx::DispatchArray& getPasses() { return mPasses; } - void setPasses(fx::DispatchArray&& passes); + void setPasses(Fx::DispatchArray&& passes); void setMask(bool underwater, bool exterior); @@ -60,8 +60,8 @@ namespace MWRender bool mAvgLum = false; bool mPostprocessing = false; - fx::DispatchArray mPasses; - fx::FlagsType mMask = 0; + Fx::DispatchArray mPasses; + Fx::FlagsType mMask = 0; osg::ref_ptr mFallbackProgram; osg::ref_ptr mMultiviewResolveProgram; @@ -74,7 +74,7 @@ namespace MWRender osg::ref_ptr mTextureDistortion; mutable bool mDirty = false; - mutable std::vector mDirtyAttachments; + mutable std::vector mDirtyAttachments; mutable osg::ref_ptr mRenderViewport; mutable osg::ref_ptr mMultiviewResolveFramebuffer; mutable osg::ref_ptr mDestinationFBO; diff --git a/apps/openmw/mwrender/postprocessor.cpp b/apps/openmw/mwrender/postprocessor.cpp index 1f1b7258b3..365429a4a8 100644 --- a/apps/openmw/mwrender/postprocessor.cpp +++ b/apps/openmw/mwrender/postprocessor.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -206,7 +207,7 @@ namespace MWRender mGLSLVersion = ext->glslLanguageVersion * 100; mUBO = ext->isUniformBufferObjectSupported && mGLSLVersion >= 330; - mStateUpdater = new fx::StateUpdater(mUBO); + mStateUpdater = new Fx::StateUpdater(mUBO); addChild(mHUDCamera); addChild(mRootNode); @@ -250,14 +251,12 @@ namespace MWRender void PostProcessor::populateTechniqueFiles() { - for (const auto& name : mVFS->getRecursiveDirectoryIterator(fx::Technique::sSubdir)) + for (const auto& path : mVFS->getRecursiveDirectoryIterator(Fx::Technique::sSubdir)) { - std::filesystem::path path = Files::pathFromUnicodeString(name); - std::string fileExt = Misc::StringUtils::lowerCase(Files::pathToUnicodeString(path.extension())); - if (!path.parent_path().has_parent_path() && fileExt == fx::Technique::sExt) + std::string_view fileExt = Misc::getFileExtension(path); + if (path.parent().parent().empty() && fileExt == Fx::Technique::sExt) { - const auto absolutePath = mVFS->getAbsoluteFileName(path); - mTechniqueFileMap[Files::pathToUnicodeString(absolutePath.stem())] = absolutePath; + mTechniqueFiles.emplace(path); } } } @@ -348,10 +347,10 @@ namespace MWRender for (auto& technique : mTechniques) { - if (technique->getStatus() == fx::Technique::Status::File_Not_exists) + if (technique->getStatus() == Fx::Technique::Status::File_Not_exists) continue; - const auto lastWriteTime = std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()]); + const auto lastWriteTime = mVFS->getLastModified(technique->getFileName()); const bool isDirty = technique->setLastModificationTime(lastWriteTime); if (!isDirty) @@ -363,7 +362,7 @@ namespace MWRender std::this_thread::sleep_for(std::chrono::milliseconds(5)); if (technique->compile()) - Log(Debug::Info) << "Reloaded technique : " << mTechniqueFileMap[technique->getName()]; + Log(Debug::Info) << "Reloaded technique : " << technique->getFileName(); mReload = technique->isValid(); } @@ -401,7 +400,7 @@ namespace MWRender createObjectsForFrame(frameId); mDirty = false; - mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); + mCanvases[frameId]->setPasses(Fx::DispatchArray(mTemplateData)); } if ((mNormalsSupported && mNormals != mPrevNormals) || (mPassLights != mPrevPassLights)) @@ -566,7 +565,7 @@ namespace MWRender mNormals = false; mPassLights = false; - std::vector attachmentsToDirty; + std::vector attachmentsToDirty; for (const auto& technique : mTechniques) { @@ -580,7 +579,7 @@ namespace MWRender continue; } - fx::DispatchNode node; + Fx::DispatchNode node; node.mFlags = technique->getFlags(); @@ -593,7 +592,7 @@ namespace MWRender if (technique->getLights()) mPassLights = true; - if (node.mFlags & fx::Technique::Flag_Disable_SunGlare) + if (node.mFlags & Fx::Technique::Flag_Disable_SunGlare) sunglare = false; // required default samplers available to every shader pass @@ -639,7 +638,7 @@ namespace MWRender for (const auto& pass : technique->getPasses()) { int subTexUnit = texUnit; - fx::DispatchNode::SubPass subPass; + Fx::DispatchNode::SubPass subPass; pass->prepareStateSet(subPass.mStateSet, technique->getName()); @@ -655,6 +654,14 @@ namespace MWRender const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight()); subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h)); + if (subPass.mMipMap) + { + subPass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h)); + } + else + { + subPass.mRenderTexture->setNumMipmapLevels(0); + } subPass.mRenderTexture->setTextureSize(w, h); subPass.mRenderTexture->dirtyTextureObject(); @@ -666,7 +673,7 @@ namespace MWRender [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) == attachmentsToDirty.cend()) { - attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + attachmentsToDirty.push_back(Fx::Types::RenderTarget(renderTarget)); } } @@ -685,7 +692,7 @@ namespace MWRender [renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; }) == attachmentsToDirty.cend()) { - attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget)); + attachmentsToDirty.push_back(Fx::Types::RenderTarget(renderTarget)); } subTexUnit++; } @@ -698,7 +705,7 @@ namespace MWRender mTemplateData.emplace_back(std::move(node)); } - mCanvases[frameId]->setPasses(fx::DispatchArray(mTemplateData)); + mCanvases[frameId]->setPasses(Fx::DispatchArray(mTemplateData)); if (auto hud = MWBase::Environment::get().getWindowManager()->getPostProcessorHud()) hud->updateTechniques(); @@ -710,7 +717,7 @@ namespace MWRender } PostProcessor::Status PostProcessor::enableTechnique( - std::shared_ptr technique, std::optional location) + std::shared_ptr technique, std::optional location) { if (technique->getLocked() || (location.has_value() && location.value() < 0)) return Status_Error; @@ -725,7 +732,7 @@ namespace MWRender return Status_Toggled; } - PostProcessor::Status PostProcessor::disableTechnique(std::shared_ptr technique, bool dirty) + PostProcessor::Status PostProcessor::disableTechnique(std::shared_ptr technique, bool dirty) { if (technique->getLocked()) return Status_Error; @@ -741,7 +748,7 @@ namespace MWRender return Status_Toggled; } - bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const + bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const { if (auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); it == mTechniques.end()) return false; @@ -749,28 +756,35 @@ namespace MWRender return technique->isValid(); } - std::shared_ptr PostProcessor::loadTechnique(const std::string& name, bool loadNextFrame) + std::shared_ptr PostProcessor::loadTechnique(std::string_view name, bool loadNextFrame) + { + VFS::Path::Normalized path = Fx::Technique::makeFileName(name); + return loadTechnique(VFS::Path::NormalizedView(path), loadNextFrame); + } + + std::shared_ptr PostProcessor::loadTechnique(VFS::Path::NormalizedView path, bool loadNextFrame) { for (const auto& technique : mTemplates) - if (Misc::StringUtils::ciEqual(technique->getName(), name)) + if (technique->getFileName() == path) return technique; for (const auto& technique : mQueuedTemplates) - if (Misc::StringUtils::ciEqual(technique->getName(), name)) + if (technique->getFileName() == path) return technique; - std::string realName = name; - auto fileIter = mTechniqueFileMap.find(name); - if (fileIter != mTechniqueFileMap.end()) - realName = fileIter->first; + std::string name; + if (mTechniqueFiles.contains(path)) + name = mVFS->getStem(path); + else + name = path.stem(); - auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), - std::move(realName), renderWidth(), renderHeight(), mUBO, mNormalsSupported); + auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), + path, std::move(name), renderWidth(), renderHeight(), mUBO, mNormalsSupported); technique->compile(); - if (technique->getStatus() != fx::Technique::Status::File_Not_exists) - technique->setLastModificationTime(std::filesystem::last_write_time(fileIter->second)); + if (technique->getStatus() != Fx::Technique::Status::File_Not_exists) + technique->setLastModificationTime(mVFS->getLastModified(path)); if (loadNextFrame) { @@ -825,7 +839,11 @@ namespace MWRender void PostProcessor::toggleMode() { for (auto& technique : mTemplates) + { + if (technique->getStatus() == Fx::Technique::Status::File_Not_exists) + continue; technique->compile(); + } dirtyTechniques(true); } diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index 6b1f4612f1..f81a50e9d6 100644 --- a/apps/openmw/mwrender/postprocessor.hpp +++ b/apps/openmw/mwrender/postprocessor.hpp @@ -58,7 +58,7 @@ namespace MWRender public: using FBOArray = std::array, 6>; using TextureArray = std::array, 6>; - using TechniqueList = std::vector>; + using TechniqueList = std::vector>; enum TextureIndex { @@ -122,24 +122,24 @@ namespace MWRender osg::ref_ptr getHUDCamera() { return mHUDCamera; } - osg::ref_ptr getStateUpdater() { return mStateUpdater; } + osg::ref_ptr getStateUpdater() { return mStateUpdater; } const TechniqueList& getTechniques() { return mTechniques; } const TechniqueList& getTemplates() const { return mTemplates; } - const auto& getTechniqueMap() const { return mTechniqueFileMap; } + const auto& getTechniqueFiles() const { return mTechniqueFiles; } void resize(); - Status enableTechnique(std::shared_ptr technique, std::optional location = std::nullopt); + Status enableTechnique(std::shared_ptr technique, std::optional location = std::nullopt); - Status disableTechnique(std::shared_ptr technique, bool dirty = true); + Status disableTechnique(std::shared_ptr technique, bool dirty = true); bool getSupportsNormalsRT() const { return mNormalsSupported; } template - void setUniform(std::shared_ptr technique, const std::string& name, const T& value) + void setUniform(std::shared_ptr technique, const std::string& name, const T& value) { if (!isEnabled()) return; @@ -158,7 +158,7 @@ namespace MWRender (*it)->setValue(value); } - std::optional getUniformSize(std::shared_ptr technique, const std::string& name) + std::optional getUniformSize(std::shared_ptr technique, const std::string& name) { auto it = technique->findUniform(name); @@ -168,7 +168,7 @@ namespace MWRender return (*it)->getNumElements(); } - bool isTechniqueEnabled(const std::shared_ptr& technique) const; + bool isTechniqueEnabled(const std::shared_ptr& technique) const; void setExteriorFlag(bool exterior) { mExteriorFlag = exterior; } @@ -176,7 +176,8 @@ namespace MWRender void toggleMode(); - std::shared_ptr loadTechnique(const std::string& name, bool loadNextFrame = false); + std::shared_ptr loadTechnique(VFS::Path::NormalizedView path, bool loadNextFrame = false); + std::shared_ptr loadTechnique(std::string_view name, bool loadNextFrame = false); TechniqueList getChain(); @@ -232,8 +233,7 @@ namespace MWRender TechniqueList mQueuedTemplates; TechniqueList mInternalTechniques; - std::unordered_map - mTechniqueFileMap; + std::unordered_set> mTechniqueFiles; RenderingManager& mRendering; osgViewer::Viewer* mViewer; @@ -263,13 +263,13 @@ namespace MWRender int mHeight; int mSamples; - osg::ref_ptr mStateUpdater; + osg::ref_ptr mStateUpdater; osg::ref_ptr mPingPongCull; std::array, 2> mCanvases; osg::ref_ptr mTransparentDepthPostPass; osg::ref_ptr mDistortionCallback; - fx::DispatchArray mTemplateData; + Fx::DispatchArray mTemplateData; }; } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 6afbfcfe7d..0698e8c4ae 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -943,8 +943,8 @@ namespace MWRender stateUpdater->setIsUnderwater(isUnderwater); stateUpdater->setFogColor(fogColor); stateUpdater->setGameHour(world->getTimeStamp().getHour()); - stateUpdater->setWeatherId(world->getCurrentWeather()); - stateUpdater->setNextWeatherId(world->getNextWeather()); + stateUpdater->setWeatherId(world->getCurrentWeatherScriptId()); + stateUpdater->setNextWeatherId(world->getNextWeatherScriptId()); stateUpdater->setWeatherTransition(world->getWeatherTransition()); stateUpdater->setWindSpeed(world->getWindSpeed()); stateUpdater->setSkyColor(mSky->getSkyColor()); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 2e573f8276..4723c59b8a 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -139,6 +139,7 @@ namespace MWRender int skyGetSecundaPhase() const; void skySetMoonColour(bool red); + const osg::Vec4f& getSunLightPosition() const { return mSunLight->getPosition(); } void setSunDirection(const osg::Vec3f& direction); void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis); void setNight(bool isNight) { mNight = isNight; } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 81e44248ac..81688a3444 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -565,6 +565,8 @@ namespace MWRender else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); + mResourceSystem->getSceneManager()->setUpNormalsRTForStateSet(mWaterGeom->getOrCreateStateSet(), true); + updateVisible(); } diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index a91a585367..e5aa3b1f91 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -22,6 +22,7 @@ #include "../mwmechanics/aitravel.hpp" #include "../mwmechanics/aiwander.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/greetingstate.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -487,7 +488,7 @@ namespace MWScript else if (testedTargetId == "Player") // Currently the player ID is hardcoded { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); - bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; + bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::GreetingState::InProgress; bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); } diff --git a/apps/openmw/mwscript/controlextensions.cpp b/apps/openmw/mwscript/controlextensions.cpp index b9e8f8965a..3a70ec5142 100644 --- a/apps/openmw/mwscript/controlextensions.cpp +++ b/apps/openmw/mwscript/controlextensions.cpp @@ -164,14 +164,7 @@ namespace MWScript void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = MWBase::Environment::get().getWorld()->getPlayerPtr(); - MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - MWBase::World* world = MWBase::Environment::get().getWorld(); - - bool stanceOn = stats.getStance(MWMechanics::CreatureStats::Stance_Run); - bool running = MWBase::Environment::get().getMechanicsManager()->isRunning(ptr); - bool inair = !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); - - runtime.push(stanceOn && (running || inair)); + runtime.push(MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); } }; diff --git a/apps/openmw/mwscript/dialogueextensions.cpp b/apps/openmw/mwscript/dialogueextensions.cpp index 6511fbdb01..6c219a52a3 100644 --- a/apps/openmw/mwscript/dialogueextensions.cpp +++ b/apps/openmw/mwscript/dialogueextensions.cpp @@ -82,6 +82,39 @@ namespace MWScript } }; + class OpFillJournal : public Interpreter::Opcode0 + { + public: + void execute(Interpreter::Runtime& runtime) override + { + const MWWorld::Store& dialogues + = MWBase::Environment::get().getESMStore()->get(); + MWWorld::Ptr playerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + MWBase::Journal* journal = MWBase::Environment::get().getJournal(); + MWBase::DialogueManager* dialogueManager = MWBase::Environment::get().getDialogueManager(); + + for (const auto& dialogue : dialogues) + { + if (dialogue.mType == ESM::Dialogue::Type::Journal) + { + for (const auto& journalInfo : dialogue.mInfoOrder.getOrderedInfo()) + { + if (journalInfo.mQuestStatus != ESM::DialInfo::QS_Name) + journal->addEntry(dialogue.mId, journalInfo.mData.mJournalIndex, playerPtr); + } + } + else if (dialogue.mType == ESM::Dialogue::Type::Topic) + { + for (const auto& topicInfo : dialogue.mInfoOrder.getOrderedInfo()) + { + journal->addTopic(dialogue.mId, topicInfo.mId, playerPtr); + } + dialogueManager->addTopic(dialogue.mId); + } + } + } + }; + class OpAddTopic : public Interpreter::Opcode0 { public: @@ -288,6 +321,7 @@ namespace MWScript interpreter.installSegment5>(Compiler::Dialogue::opcodeJournalExplicit); interpreter.installSegment5(Compiler::Dialogue::opcodeSetJournalIndex); interpreter.installSegment5(Compiler::Dialogue::opcodeGetJournalIndex); + interpreter.installSegment5(Compiler::Dialogue::opcodeFillJournal); interpreter.installSegment5(Compiler::Dialogue::opcodeAddTopic); interpreter.installSegment3(Compiler::Dialogue::opcodeChoice); interpreter.installSegment5>(Compiler::Dialogue::opcodeForceGreeting); diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index d34c39c9df..56eb1c2351 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -485,5 +485,6 @@ op 0x2000322: GetPCVisionBonus op 0x2000323: SetPCVisionBonus op 0x2000324: ModPCVisionBonus op 0x2000325: TestModels, T3D +op 0x2000326: FillJournal -opcodes 0x2000326-0x3ffffff unused +opcodes 0x2000327-0x3ffffff unused diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp index 15c9100b98..f6c20c9c10 100644 --- a/apps/openmw/mwscript/interpretercontext.cpp +++ b/apps/openmw/mwscript/interpretercontext.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "../mwworld/esmstore.hpp" @@ -300,26 +301,43 @@ namespace MWScript std::string_view InterpreterContext::getNPCFaction() const { - const ESM::NPC* npc = getReferenceImp().get()->mBase; - const ESM::Faction* faction = MWBase::Environment::get().getESMStore()->get().find(npc->mFaction); + const MWWorld::Ptr& ptr = getReferenceImp(); + const ESM::RefId& factionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId.empty()) + { + Log(Debug::Warning) << "getNPCFaction(): NPC " << ptr.getCellRef().getRefId() << " has no primary faction"; + return "%"; + } + + MWBase::World* world = MWBase::Environment::get().getWorld(); + const MWWorld::ESMStore& store = world->getStore(); + const ESM::Faction* faction = store.get().find(factionId); return faction->mName; } std::string_view InterpreterContext::getNPCRank() const { const MWWorld::Ptr& ptr = getReferenceImp(); - const ESM::RefId& faction = ptr.getClass().getPrimaryFaction(ptr); - if (faction.empty()) - throw std::runtime_error("getNPCRank(): NPC is not in a faction"); - - int rank = ptr.getClass().getPrimaryFactionRank(ptr); - if (rank < 0 || rank > 9) - throw std::runtime_error("getNPCRank(): invalid rank"); + const MWWorld::Class& ptrClass = ptr.getClass(); + const ESM::RefId& factionId = ptrClass.getPrimaryFaction(ptr); + if (factionId.empty()) + { + Log(Debug::Warning) << "getNPCRank(): NPC " << ptr.getCellRef().getRefId() << " has no primary faction"; + return "%"; + } MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::ESMStore& store = world->getStore(); - const ESM::Faction* fact = store.get().find(faction); - return fact->mRanks[rank]; + const ESM::Faction* faction = store.get().find(factionId); + + int rank = ptrClass.getPrimaryFactionRank(ptr); + if (rank < 0 || rank > 9) + { + Log(Debug::Warning) << "getNPCRank(): NPC " << ptr.getCellRef().getRefId() << " has invalid rank " << rank + << " in faction " << factionId; + return "%"; + } + return faction->mRanks[rank]; } std::string_view InterpreterContext::getPCName() const @@ -344,13 +362,16 @@ namespace MWScript std::string_view InterpreterContext::getPCRank() const { + const MWWorld::Ptr& ptr = getReferenceImp(); + const ESM::RefId& factionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId.empty()) + { + Log(Debug::Warning) << "getPCRank(): NPC " << ptr.getCellRef().getRefId() << " has no primary faction"; + return "%"; + } + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - - const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); - if (factionId.empty()) - throw std::runtime_error("getPCRank(): NPC is not in a faction"); - const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); auto it = ranks.find(factionId); int rank = -1; @@ -373,13 +394,16 @@ namespace MWScript std::string_view InterpreterContext::getPCNextRank() const { + const MWWorld::Ptr& ptr = getReferenceImp(); + const ESM::RefId& factionId = ptr.getClass().getPrimaryFaction(ptr); + if (factionId.empty()) + { + Log(Debug::Warning) << "getPCNextRank(): NPC " << ptr.getCellRef().getRefId() << " has no primary faction"; + return "%"; + } + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); - - const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); - if (factionId.empty()) - throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); - const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); auto it = ranks.find(factionId); int rank = -1; diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 06abbe5bb2..fd2064463e 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -615,7 +615,7 @@ namespace MWScript long key; - if (const auto k = ::Misc::StringUtils::toNumeric(effect.data()); + if (const auto k = ::Misc::StringUtils::toNumeric(effect); k.has_value() && *k >= 0 && *k <= 32767) key = *k; else @@ -1146,9 +1146,18 @@ namespace MWScript { void printLocalVars(Interpreter::Runtime& runtime, const MWWorld::Ptr& ptr) { - std::stringstream str; + std::ostringstream str; const ESM::RefId& script = ptr.getClass().getScript(ptr); + + auto printVariables = [&str](const auto& names, const auto& values, std::string_view type) { + size_t size = std::min(names.size(), values.size()); + for (size_t i = 0; i < size; ++i) + { + str << "\n " << names[i] << " = " << values[i] << " (" << type << ")"; + } + }; + if (script.empty()) str << ptr.getCellRef().getRefId() << " does not have a script."; else @@ -1159,27 +1168,9 @@ namespace MWScript const Compiler::Locals& complocals = MWBase::Environment::get().getScriptManager()->getLocals(script); - const std::vector* names = &complocals.get('s'); - for (size_t i = 0; i < names->size(); ++i) - { - if (i >= locals.mShorts.size()) - break; - str << std::endl << " " << (*names)[i] << " = " << locals.mShorts[i] << " (short)"; - } - names = &complocals.get('l'); - for (size_t i = 0; i < names->size(); ++i) - { - if (i >= locals.mLongs.size()) - break; - str << std::endl << " " << (*names)[i] << " = " << locals.mLongs[i] << " (long)"; - } - names = &complocals.get('f'); - for (size_t i = 0; i < names->size(); ++i) - { - if (i >= locals.mFloats.size()) - break; - str << std::endl << " " << (*names)[i] << " = " << locals.mFloats[i] << " (float)"; - } + printVariables(complocals.get('s'), locals.mShorts, "short"); + printVariables(complocals.get('l'), locals.mLongs, "long"); + printVariables(complocals.get('f'), locals.mFloats, "float"); } runtime.getContext().report(str.str()); @@ -1187,50 +1178,43 @@ namespace MWScript void printGlobalVars(Interpreter::Runtime& runtime) { - std::stringstream str; - str << "Global variables:"; + std::ostringstream str; + str << "Global Variables:"; MWBase::World* world = MWBase::Environment::get().getWorld(); - std::vector names = runtime.getContext().getGlobals(); + auto& context = runtime.getContext(); + std::vector names = context.getGlobals(); - // sort for user convenience - std::sort(names.begin(), names.end()); - - for (size_t i = 0; i < names.size(); ++i) - { - char type = world->getGlobalVariableType(names[i]); - str << std::endl << " " << names[i] << " = "; + std::sort(names.begin(), names.end(), ::Misc::StringUtils::ciLess); + auto printVariable = [&str, &context](const std::string& name, char type) { + str << "\n " << name << " = "; switch (type) { case 's': - - str << runtime.getContext().getGlobalShort(names[i]) << " (short)"; + str << context.getGlobalShort(name) << " (short)"; break; - case 'l': - - str << runtime.getContext().getGlobalLong(names[i]) << " (long)"; + str << context.getGlobalLong(name) << " (long)"; break; - case 'f': - - str << runtime.getContext().getGlobalFloat(names[i]) << " (float)"; + str << context.getGlobalFloat(name) << " (float)"; break; - default: - str << ""; } - } + }; - runtime.getContext().report(str.str()); + for (const auto& name : names) + printVariable(name, world->getGlobalVariableType(name)); + + context.report(str.str()); } void printGlobalScriptsVars(Interpreter::Runtime& runtime) { - std::stringstream str; - str << std::endl << "Global Scripts:"; + std::ostringstream str; + str << "\nGlobal Scripts:"; const auto& scripts = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScripts(); @@ -1238,12 +1222,11 @@ namespace MWScript std::map> globalScripts(scripts.begin(), scripts.end()); auto printVariables - = [&str](const ESM::RefId& scptName, const auto& names, const auto& values, std::string_view type) { + = [&str](std::string_view scptName, const auto& names, const auto& values, std::string_view type) { size_t size = std::min(names.size(), values.size()); for (size_t i = 0; i < size; ++i) { - str << std::endl - << " " << scptName << "->" << names[i] << " = " << values[i] << " (" << type << ")"; + str << "\n " << scptName << "->" << names[i] << " = " << values[i] << " (" << type << ")"; } }; @@ -1253,18 +1236,20 @@ namespace MWScript if (!script->mRunning) continue; + std::string_view scptName = refId.getRefIdString(); + const Compiler::Locals& complocals = MWBase::Environment::get().getScriptManager()->getLocals(refId); const Locals& locals = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(refId); if (locals.isEmpty()) - str << std::endl << " No variables in script " << refId; + str << "\n No variables in script " << scptName; else { - printVariables(refId, complocals.get('s'), locals.mShorts, "short"); - printVariables(refId, complocals.get('l'), locals.mLongs, "long"); - printVariables(refId, complocals.get('f'), locals.mFloats, "float"); + printVariables(scptName, complocals.get('s'), locals.mShorts, "short"); + printVariables(scptName, complocals.get('l'), locals.mLongs, "long"); + printVariables(scptName, complocals.get('f'), locals.mFloats, "float"); } } diff --git a/apps/openmw/mwscript/skyextensions.cpp b/apps/openmw/mwscript/skyextensions.cpp index d2b41fb87a..b354bb5ce2 100644 --- a/apps/openmw/mwscript/skyextensions.cpp +++ b/apps/openmw/mwscript/skyextensions.cpp @@ -71,7 +71,7 @@ namespace MWScript public: void execute(Interpreter::Runtime& runtime) override { - runtime.push(MWBase::Environment::get().getWorld()->getCurrentWeather()); + runtime.push(MWBase::Environment::get().getWorld()->getCurrentWeatherScriptId()); } }; diff --git a/apps/openmw/mwsound/openaloutput.cpp b/apps/openmw/mwsound/openaloutput.cpp index 0f27524912..60c9e5f3ea 100644 --- a/apps/openmw/mwsound/openaloutput.cpp +++ b/apps/openmw/mwsound/openaloutput.cpp @@ -34,7 +34,7 @@ namespace { - const int sLoudnessFPS = 20; // loudness values per second of audio + const float sLoudnessFPS = 20.0f; // loudness values per second of audio ALCenum checkALCError(ALCdevice* device, const char* func, int line) { diff --git a/apps/openmw/mwsound/soundbuffer.cpp b/apps/openmw/mwsound/soundbuffer.cpp index 722d89f0eb..0c10ba5552 100644 --- a/apps/openmw/mwsound/soundbuffer.cpp +++ b/apps/openmw/mwsound/soundbuffer.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include @@ -99,7 +102,12 @@ namespace MWSound { if (mBufferNameMap.empty()) { - for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get()) + const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore(); + for (const ESM::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::Sound& sound : esmstore->get()) + insertSound(sound.mId, sound); + for (const ESM4::SoundReference& sound : esmstore->get()) insertSound(sound.mId, sound); } @@ -190,6 +198,28 @@ namespace MWSound return &sfx; } + SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound) + { + std::string path = Misc::ResourceHelpers::correctResourcePath( + { { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3"); + float volume = 1, min = 1, max = 255; // TODO: needs research + SoundBuffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max); + mBufferNameMap.emplace(soundId, &sfx); + return &sfx; + } + + SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound) + { + std::string path = Misc::ResourceHelpers::correctResourcePath( + { { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3"); + float volume = 1, min = 1, max = 255; // TODO: needs research + // TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to + // ESMStore. + SoundBuffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max); + mBufferNameMap.emplace(soundId, &sfx); + return &sfx; + } + void SoundBufferPool::unloadUnused() { while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) diff --git a/apps/openmw/mwsound/soundbuffer.hpp b/apps/openmw/mwsound/soundbuffer.hpp index f7e7639b2d..493577a9c3 100644 --- a/apps/openmw/mwsound/soundbuffer.hpp +++ b/apps/openmw/mwsound/soundbuffer.hpp @@ -15,6 +15,12 @@ namespace ESM struct Sound; } +namespace ESM4 +{ + struct Sound; + struct SoundReference; +} + namespace VFS { class Manager; @@ -112,8 +118,10 @@ namespace MWSound // NOTE: unused buffers are stored in front-newest order. std::deque mUnusedBuffers; - inline SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); - inline SoundBuffer* insertSound(std::string_view fileName); + SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); + SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound); + SoundBuffer* insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound); + SoundBuffer* insertSound(std::string_view fileName); inline void unloadUnused(); }; diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index 6d2583776b..02d993d186 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -81,8 +81,7 @@ MWState::Character* MWState::CharacterManager::createCharacter(const std::string path = mPath / test.str(); } - mCharacters.emplace_back(path, mGame); - return &mCharacters.back(); + return &mCharacters.emplace_front(path, mGame); } std::list::iterator MWState::CharacterManager::findCharacter(const MWState::Character* character) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 498d2ab25a..34aa3eaa46 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -332,14 +332,15 @@ void MWState::StateManager::saveGame(std::string_view description, const Slot* s writer.close(); if (stream.fail()) - throw std::runtime_error("Write operation failed (memory stream)"); + throw std::runtime_error( + "Write operation failed (memory stream): " + std::generic_category().message(errno)); // All good, write to file std::ofstream filestream(slot->mPath, std::ios::binary); filestream << stream.rdbuf(); if (filestream.fail()) - throw std::runtime_error("Write operation failed (file stream)"); + throw std::runtime_error("Write operation failed (file stream): " + std::generic_category().message(errno)); Settings::saves().mCharacter.set(Files::pathToUnicodeString(slot->mPath.parent_path().filename())); mLastSavegame = slot->mPath; diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index 58e6f013aa..dbc548f585 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -21,12 +21,11 @@ namespace MWWorld MWWorld::Ptr object = getTarget(); MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); - if (object.getClass().hasItemHealth(object) && object.getCellRef().getCharge() == 0) + if (actor != MWMechanics::getPlayer()) { - if (actor == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); - - return; + // player logic is handled in InventoryWindow::useItem + if (object.getClass().hasItemHealth(object) && object.getCellRef().getCharge() == 0) + return; } if (!mForce) diff --git a/apps/openmw/mwworld/actionharvest.cpp b/apps/openmw/mwworld/actionharvest.cpp index 30f316c2db..1d9e009afe 100644 --- a/apps/openmw/mwworld/actionharvest.cpp +++ b/apps/openmw/mwworld/actionharvest.cpp @@ -11,6 +11,8 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/animation.hpp" + #include "class.hpp" #include "containerstore.hpp" @@ -89,8 +91,9 @@ namespace MWWorld MWBase::Environment::get().getWindowManager()->messageBox(tooltip); } - // Update animation object - MWBase::Environment::get().getWorld()->disable(target); - MWBase::Environment::get().getWorld()->enable(target); + auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(target); + if (anim != nullptr) + anim->harvest(target); } } diff --git a/apps/openmw/mwworld/cellref.hpp b/apps/openmw/mwworld/cellref.hpp index 4dcac4def5..c290e2733c 100644 --- a/apps/openmw/mwworld/cellref.hpp +++ b/apps/openmw/mwworld/cellref.hpp @@ -244,6 +244,18 @@ namespace MWWorld return std::abs(count); return count; } + + unsigned getAbsCount() const + { + struct Visitor + { + int operator()(const ESM::CellRef& ref) { return ref.mCount; } + int operator()(const ESM4::Reference& ref) { return ref.mCount; } + int operator()(const ESM4::ActorCharacter& ref) { return ref.mCount; } + }; + return static_cast(std::abs(std::visit(Visitor(), mCellRef.mVariant))); + } + void setCount(int value); // Write the content of this CellRef into the given ObjectState diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index fca0135e13..75ee14f627 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -1263,26 +1263,48 @@ namespace MWWorld } } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + // Actors need to respawn here even if they've been moved to another cell + for (LiveCellRefBase& base : get().mList) { - Ptr ptr = getCurrentPtr(&*it); + Ptr ptr = getCurrentPtr(&base); clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - for (CellRefList::List::iterator it(get().mList.begin()); - it != get().mList.end(); ++it) + for (LiveCellRefBase& base : get().mList) { - Ptr ptr = getCurrentPtr(&*it); + Ptr ptr = getCurrentPtr(&base); clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } - forEachType([](Ptr ptr) { - // no need to clearCorpse, handled as part of get() + for (LiveCellRefBase& base : get().mList) + { + Ptr ptr = getCurrentPtr(&base); if (!ptr.mRef->isDeleted()) ptr.getClass().respawn(ptr); - return true; - }); + } + for (const auto& [base, _] : mMovedHere) + { + switch (base->getType()) + { + case ESM::Creature::sRecordId: + case ESM::NPC::sRecordId: + case ESM::CreatureLevList::sRecordId: + { + MWWorld::Ptr ptr(base, this); + if (ptr.mRef->isDeleted()) + continue; + // Remove actors that have been dead a while, but don't belong here and didn't get hit by the + // logic above + if (ptr.getClass().isActor()) + clearCorpse(ptr, mStore); + else // Respawn lists in their new position + ptr.getClass().respawn(ptr); + break; + } + default: + break; + } + } } } @@ -1354,6 +1376,15 @@ namespace MWWorld return {}; } + CellStore* MWWorld::CellStore::getOriginCell(const Ptr& object) const + { + MovedRefTracker::const_iterator found = mMovedHere.find(object.getBase()); + if (found != mMovedHere.end()) + return found->second; + + return object.getCell(); + } + Ptr CellStore::getPtr(ESM::RefId id) { if (mState == CellStore::State_Unloaded) diff --git a/apps/openmw/mwworld/cellstore.hpp b/apps/openmw/mwworld/cellstore.hpp index 126935ace5..59127d6186 100644 --- a/apps/openmw/mwworld/cellstore.hpp +++ b/apps/openmw/mwworld/cellstore.hpp @@ -338,6 +338,8 @@ namespace MWWorld Ptr getMovedActor(int actorId) const; + CellStore* getOriginCell(const Ptr& object) const; + Ptr getPtr(ESM::RefId id); private: diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 105fbca80a..fc862f302f 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -119,8 +119,8 @@ namespace MWWorld throw std::runtime_error("class cannot hit"); } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, - const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const + void Class::onHit(const Ptr& ptr, const std::map& damages, const Ptr& object, + const Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } @@ -205,7 +205,7 @@ namespace MWWorld return std::make_pair(std::vector(), false); } - ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const + ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const { return {}; } @@ -235,7 +235,7 @@ namespace MWWorld return false; } - float Class::getArmorRating(const MWWorld::Ptr& ptr) const + float Class::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const { throw std::runtime_error("Class does not support armor rating"); } @@ -452,11 +452,6 @@ namespace MWWorld throw std::runtime_error("class does not support skills"); } - int Class::getBloodTexture(const MWWorld::ConstPtr& ptr) const - { - throw std::runtime_error("class does not support gore"); - } - void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} @@ -514,7 +509,8 @@ namespace MWWorld return -1; } - float Class::getEffectiveArmorRating(const ConstPtr& armor, const Ptr& actor) const + float Class::getSkillAdjustedArmorRating( + const ConstPtr& armor, const Ptr& actor, bool useLuaInterfaceIfAvailable) const { throw std::runtime_error("class does not support armor ratings"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index d3d75aa935..5633c90b12 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -6,7 +6,6 @@ #include #include -#include #include #include "doorstate.hpp" @@ -16,9 +15,13 @@ #include "../mwmechanics/damagesourcetype.hpp" #include -#include #include +namespace osg +{ + class Quat; +} + namespace ESM { struct ObjectState; @@ -144,11 +147,11 @@ namespace MWWorld /// enums. ignored for creature attacks. /// (default implementation: throw an exception) - virtual void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object, - const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, + virtual void onHit(const MWWorld::Ptr& ptr, const std::map& damages, + const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const; - ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is - /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the + ///< Alerts \a ptr that it's being hit for \a damages by \a object (sword, arrow, etc). \a attacker specifies + ///< the /// actor responsible for the attack. \a successful specifies if the hit is /// successful or not. \a sourceType classifies the damage source. @@ -209,7 +212,7 @@ namespace MWWorld /// /// Default implementation: return (empty vector, false). - virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr) const; + virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable = false) const; /// Return the index of the skill this item corresponds to when equipped. /// (default implementation: return empty ref id) @@ -255,7 +258,7 @@ namespace MWWorld virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const; ///< Returns the sound ID for \a ptr of the given soundgen \a type. - virtual float getArmorRating(const MWWorld::Ptr& ptr) const; + virtual float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable = false) const; ///< @return combined armor rating of this actor virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const; @@ -310,9 +313,6 @@ namespace MWWorld virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; } ///< Return whether this class of object can be activated with telekinesis - /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) - virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const; - virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const; // Similar to `copyToCell`, but preserves RefNum and moves LuaScripts. @@ -375,7 +375,8 @@ namespace MWWorld virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const; /// Get the effective armor rating, factoring in the actor's skills, for the given armor. - virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const; + virtual float getSkillAdjustedArmorRating( + const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable = false) const; virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index ea183b6b53..7262805f81 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -566,10 +566,10 @@ namespace MWWorld std::vector refs; std::set keyIDs; std::vector refIDs; - Store Cells = get(); - for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it) + const Store& cells = get(); + for (auto it = cells.intBegin(); it != cells.intEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); - for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it) + for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) readRefs(*it, refs, refIDs, keyIDs, readers); const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum); diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 6c71ae0052..0c37f243e8 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -105,6 +105,8 @@ namespace ESM4 struct Potion; struct Race; struct Reference; + struct Sound; + struct SoundReference; struct Static; struct StaticCollection; struct Terminal; @@ -146,8 +148,8 @@ namespace MWWorld Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, Store, - Store, Store, Store, Store, - Store, Store>; + Store, Store, Store, Store, + Store, Store, Store, Store>; private: template diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index f48f4e6e31..ec3355b09e 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -55,7 +55,7 @@ void MWWorld::InventoryStore::storeEquipmentState( } if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref) - inventory.mSelectedEnchantItem = index; + inventory.mSelectedEnchantItem = static_cast(index); } void MWWorld::InventoryStore::readEquipmentState( @@ -378,110 +378,111 @@ void MWWorld::InventoryStore::autoEquipWeapon(TSlots& slots_) void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) { - // Only NPCs can wear armor for now. - // For creatures we equip only shields. const Ptr& actor = getPtr(); - if (!actor.getClass().isNpc()) + + // Creatures only want shields and don't benefit from armor rating or unarmored skill + const MWWorld::Class& actorCls = actor.getClass(); + const bool actorIsNpc = actorCls.isNpc(); + + int equipmentTypes = ContainerStore::Type_Armor; + float unarmoredRating = 0.f; + if (actorIsNpc) { - autoEquipShield(slots_); - return; + equipmentTypes |= ContainerStore::Type_Clothing; + const auto& store = MWBase::Environment::get().getESMStore()->get(); + const float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); + const float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); + const float unarmoredSkill = actorCls.getSkill(actor, ESM::Skill::Unarmored); + unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); + unarmoredRating = std::max(unarmoredRating, 0.f); } - const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); - - static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); - static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); - - float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored); - float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); - - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); - ++iter) + for (ContainerStoreIterator iter(begin(equipmentTypes)); iter != end(); ++iter) { Ptr test = *iter; + const MWWorld::Class& testCls = test.getClass(); + const bool isArmor = iter.getType() == ContainerStore::Type_Armor; - switch (test.getClass().canBeEquipped(test, actor).first) + // Discard armor that is worse than unarmored for NPCs and non-shields for creatures + if (isArmor) { - case 0: - continue; - default: - break; + if (actorIsNpc) + { + if (testCls.getSkillAdjustedArmorRating(test, actor) <= unarmoredRating) + continue; + } + else + { + if (test.get()->mBase->mData.mType != ESM::Armor::Shield) + continue; + } } - if (iter.getType() == ContainerStore::Type_Armor - && test.getClass().getEffectiveArmorRating(test, actor) <= std::max(unarmoredRating, 0.f)) - { + // Don't equip the item if it cannot be equipped + if (testCls.canBeEquipped(test, actor).first == 0) continue; - } - std::pair, bool> itemsSlots = iter->getClass().getEquipmentSlots(*iter); + const auto [itemSlots, canStack] = testCls.getEquipmentSlots(test); // checking if current item pointed by iter can be equipped - for (int slot : itemsSlots.first) + for (const int slot : itemSlots) { - // if true then it means slot is equipped already // check if slot may require swapping if current item is more valuable if (slots_.at(slot) != end()) { Ptr old = *slots_.at(slot); + const MWWorld::Class& oldCls = old.getClass(); + unsigned int oldType = old.getType(); - if (iter.getType() == ContainerStore::Type_Armor) + if (!isArmor) { - if (old.getType() == ESM::Armor::sRecordId) - { - if (old.get()->mBase->mData.mType < test.get()->mBase->mData.mType) - continue; + // Armor should replace clothing and weapons, but clothing should only replace clothing + if (oldType != ESM::Clothing::sRecordId) + continue; - if (old.get()->mBase->mData.mType == test.get()->mBase->mData.mType) - { - if (old.getClass().getEffectiveArmorRating(old, actor) - >= test.getClass().getEffectiveArmorRating(test, actor)) - // old armor had better armor rating - continue; - } - } - // suitable armor should replace already equipped clothing - } - else if (iter.getType() == ContainerStore::Type_Clothing) - { - // if left ring is equipped + // If the left ring slot is filled, don't swap if the right ring is cheaper if (slot == Slot_LeftRing) { - // if there is a place for right ring dont swap it if (slots_.at(Slot_RightRing) == end()) - { continue; - } - else // if right ring is equipped too - { - Ptr rightRing = *slots_.at(Slot_RightRing); - // we want to swap cheaper ring only if both are equipped - if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing)) + Ptr rightRing = *slots_.at(Slot_RightRing); + if (rightRing.getClass().getValue(rightRing) <= oldCls.getValue(old)) + continue; + } + + if (testCls.getValue(test) <= oldCls.getValue(old)) + continue; + } + else if (oldType == ESM::Armor::sRecordId) + { + const int32_t oldArmorType = old.get()->mBase->mData.mType; + const int32_t newArmorType = test.get()->mBase->mData.mType; + if (oldArmorType == newArmorType) + { + // For NPCs, compare armor rating; for creatures, compare condition + if (actorIsNpc) + { + const float rating = testCls.getSkillAdjustedArmorRating(test, actor); + const float oldRating = oldCls.getSkillAdjustedArmorRating(old, actor); + if (rating <= oldRating) + continue; + } + else + { + if (testCls.getItemHealth(test) <= oldCls.getItemHealth(old)) continue; } } - - if (old.getType() == ESM::Clothing::sRecordId) - { - // check value - if (old.getClass().getValue(old) >= test.getClass().getValue(test)) - // old clothing was more valuable - continue; - } - else - // suitable clothing should NOT replace already equipped armor + else if (oldArmorType < newArmorType) continue; } } - if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped + // unstack the item if required + if (!canStack && test.getCellRef().getCount() > 1) { - // unstack item pointed to by iterator if required - if (iter->getCellRef().getCount() > 1) - { - unstack(*iter); - } + unstack(test); } // if we are here it means item can be equipped or swapped @@ -491,27 +492,6 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_) } } -void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_) -{ - for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter) - { - if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) - continue; - if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) - continue; - std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); - int slot = shieldSlots.first[0]; - const ContainerStoreIterator& shield = slots_[slot]; - if (shield != end() && shield.getType() == Type_Armor - && shield->get()->mBase->mData.mType == ESM::Armor::Shield) - { - if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) - continue; - } - slots_[slot] = iter; - } -} - void MWWorld::InventoryStore::autoEquip() { TSlots slots_; @@ -522,8 +502,6 @@ void MWWorld::InventoryStore::autoEquip() // Autoequip clothing, armor and weapons. // Equipping lights is handled in Actors::updateEquippedLight based on environment light. - // Note: creatures ignore equipment armor rating and only equip shields - // Use custom logic for them - select shield based on its health instead of armor rating autoEquipWeapon(slots_); autoEquipArmor(slots_); @@ -754,6 +732,16 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr& item) return false; } +bool MWWorld::InventoryStore::isEquipped(const ESM::RefId& id) +{ + for (int i = 0; i < MWWorld::InventoryStore::Slots; ++i) + { + if (getSlot(i) != end() && getSlot(i)->getCellRef().getRefId() == id) + return true; + } + return false; +} + bool MWWorld::InventoryStore::isFirstEquip() { bool first = mFirstAutoEquip; diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index 0af6ee2b28..33de6f66d3 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -69,7 +69,6 @@ namespace MWWorld void autoEquipWeapon(TSlots& slots_); void autoEquipArmor(TSlots& slots_); - void autoEquipShield(TSlots& slots_); // selected magic item (for using enchantments of type "Cast once" or "Cast when used") ContainerStoreIterator mSelectedEnchantItem; @@ -118,6 +117,7 @@ namespace MWWorld ///< \warning \a iterator can not be an end()-iterator, use unequip function instead bool isEquipped(const MWWorld::ConstPtr& item); + bool isEquipped(const ESM::RefId& id); ///< Utility function, returns true if the given item is equipped in any slot void setSelectedEnchantItem(const ContainerStoreIterator& iterator); diff --git a/apps/openmw/mwworld/refdata.hpp b/apps/openmw/mwworld/refdata.hpp index e0b62c94b6..6c4912d867 100644 --- a/apps/openmw/mwworld/refdata.hpp +++ b/apps/openmw/mwworld/refdata.hpp @@ -105,7 +105,7 @@ namespace MWWorld void setLocals(const ESM::Script& script); - MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); } + MWLua::LocalScripts* getLuaScripts() const { return mLuaScripts.get(); } void setLuaScripts(std::shared_ptr&&); /// This flag is only used for content stack loading and will not be stored in the savegame. diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 478bdb5bb8..0c9a13bc47 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -511,7 +512,7 @@ namespace MWWorld if (cellVariant.isExterior()) { - if (const auto heightField = mPhysics->getHeightField(cellX, cellY)) + if (mPhysics->getHeightField(cellX, cellY) != nullptr) mNavigator.addWater( osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE, waterLevel, navigatorUpdateGuard); } @@ -645,8 +646,11 @@ namespace MWWorld mHalfGridSize = halfGridSize; mCurrentGridCenter = osg::Vec2i(playerCellX, playerCellY); osg::Vec4i newGrid = gridCenterToBounds(mCurrentGridCenter); - mRendering.setActiveGrid(newGrid); + + // NOTE: setActiveGrid must be after enableTerrain, otherwise we set the grid in the old exterior worldspace mRendering.enableTerrain(true, playerCellIndex.mWorldspace); + mRendering.setActiveGrid(newGrid); + mPreloader->setTerrain(mRendering.getTerrain()); if (mRendering.pagingUnlockCache()) mPreloader->abortTerrainPreloadExcept(nullptr); @@ -1292,6 +1296,9 @@ namespace MWWorld void Scene::preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync) { + if (mRendering.getTerrain()->getWorldspace() != worldspace) + throw std::runtime_error("preloadTerrain can only work with the current exterior worldspace"); + ESM::ExteriorCellLocation cellPos = ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace); const PositionCellGrid position{ pos, gridCenterToBounds({ cellPos.mX, cellPos.mY }) }; mPreloader->abortTerrainPreloadExcept(&position); diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 116e52e535..1fa7779e51 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -129,6 +129,9 @@ namespace MWWorld void preloadExteriorGrid(const osg::Vec3f& playerPos, const osg::Vec3f& predictedPos); void preloadFastTravelDestinations( const osg::Vec3f& playerPos, std::vector& exteriorPositions); + void preloadCellWithSurroundings(MWWorld::CellStore& cell); + void preloadCell(MWWorld::CellStore& cell); + void preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync = false); osg::Vec4i gridCenterToBounds(const osg::Vec2i& centerCell) const; osg::Vec2i getNewGridCenter(const osg::Vec3f& pos, const osg::Vec2i* currentGridCenter = nullptr) const; @@ -143,9 +146,6 @@ namespace MWWorld ~Scene(); - void preloadCellWithSurroundings(MWWorld::CellStore& cell); - void preloadCell(MWWorld::CellStore& cell); - void preloadTerrain(const osg::Vec3f& pos, ESM::RefId worldspace, bool sync = false); void reloadTerrain(); void playerMoved(const osg::Vec3f& pos); diff --git a/apps/openmw/mwworld/store.cpp b/apps/openmw/mwworld/store.cpp index b0684b1ab4..80bcdb056a 100644 --- a/apps/openmw/mwworld/store.cpp +++ b/apps/openmw/mwworld/store.cpp @@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; +template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; template class MWWorld::TypedDynamicStore; diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 2ee77458d4..f2cffb5611 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -1,5 +1,6 @@ #include "weather.hpp" +#include #include #include @@ -141,9 +142,12 @@ namespace MWWorld return direction; } - Weather::Weather(const std::string& name, float stormWindSpeed, float rainSpeed, float dlFactor, float dlOffset, - const std::string& particleEffect) - : mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) + Weather::Weather(ESM::RefId id, int scriptId, const std::string& name, float stormWindSpeed, float rainSpeed, + float dlFactor, float dlOffset, const std::string& particleEffect) + : mId(id) + , mScriptId(scriptId) + , mName(name) + , mCloudTexture(Fallback::Map::getString("Weather_" + name + "_Cloud_Texture")) , mSkyColor(Fallback::Map::getColour("Weather_" + name + "_Sky_Sunrise_Color"), Fallback::Map::getColour("Weather_" + name + "_Sky_Day_Color"), Fallback::Map::getColour("Weather_" + name + "_Sky_Sunset_Color"), @@ -676,6 +680,41 @@ namespace MWWorld stopSounds(); } + const Weather* WeatherManager::getWeather(size_t index) const + { + if (index < mWeatherSettings.size()) + return &mWeatherSettings[index]; + + return nullptr; + } + + const Weather* WeatherManager::getWeather(const ESM::RefId& id) const + { + auto it = std::find_if( + mWeatherSettings.begin(), mWeatherSettings.end(), [id](const auto& weather) { return weather.mId == id; }); + + if (it != mWeatherSettings.end()) + return &*it; + + return nullptr; + } + + void WeatherManager::changeWeather(const ESM::RefId& regionID, const ESM::RefId& weatherID) + { + auto wIt = std::find_if(mWeatherSettings.begin(), mWeatherSettings.end(), + [weatherID](const auto& weather) { return weather.mId == weatherID; }); + + if (wIt != mWeatherSettings.end()) + { + auto rIt = mRegions.find(regionID); + if (rIt != mRegions.end()) + { + rIt->second.setWeather(std::distance(mWeatherSettings.begin(), wIt)); + regionalWeatherChanged(rIt->first, rIt->second); + } + } + } + void WeatherManager::changeWeather(const ESM::RefId& regionID, const unsigned int weatherID) { // In Morrowind, this seems to have the following behavior, when applied to the current region: @@ -1053,8 +1092,9 @@ namespace MWWorld const std::string& name, float dlFactor, float dlOffset, const std::string& particleEffect) { static const float fStromWindSpeed = mStore.get().find("fStromWindSpeed")->mValue.getFloat(); - - Weather weather(name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); + ESM::StringRefId id(name); + Weather weather( + id, mWeatherSettings.size(), name, fStromWindSpeed, mRainSpeed, dlFactor, dlOffset, particleEffect); mWeatherSettings.push_back(weather); } diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 7c27a10316..ddc96f0ae6 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -109,6 +109,11 @@ namespace MWWorld T getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const; + const T& getSunriseValue() const { return mSunriseValue; } + const T& getDayValue() const { return mDayValue; } + const T& getSunsetValue() const { return mSunsetValue; } + const T& getNightValue() const { return mNightValue; } + private: T mSunriseValue, mDayValue, mSunsetValue, mNightValue; }; @@ -119,9 +124,12 @@ namespace MWWorld public: static osg::Vec3f defaultDirection(); - Weather(const std::string& name, float stormWindSpeed, float rainSpeed, float dlFactor, float dlOffset, - const std::string& particleEffect); + Weather(const ESM::RefId id, const int scriptId, const std::string& name, float stormWindSpeed, float rainSpeed, + float dlFactor, float dlOffset, const std::string& particleEffect); + ESM::RefId mId; + int mScriptId; + std::string mName; std::string mCloudTexture; // Sky (atmosphere) color @@ -289,11 +297,12 @@ namespace MWWorld ~WeatherManager(); /** - * Change the weather in the specified region + * Change the weather in the specified region by id of the weather * @param region that should be changed * @param ID of the weather setting to shift to */ void changeWeather(const ESM::RefId& regionID, const unsigned int weatherID); + void changeWeather(const ESM::RefId& regionID, const ESM::RefId& weatherID); void modRegion(const ESM::RefId& regionID, const std::vector& chances); void playerTeleported(const ESM::RefId& playerRegion, bool isExterior); @@ -316,8 +325,23 @@ namespace MWWorld void advanceTime(double hours, bool incremental); + const std::vector& getAllWeather() { return mWeatherSettings; } + + const Weather& getWeather() { return mWeatherSettings[mCurrentWeather]; } + + const Weather* getWeather(size_t index) const; + + const Weather* getWeather(const ESM::RefId& id) const; + int getWeatherID() const { return mCurrentWeather; } + const Weather* getNextWeather() + { + if (mNextWeather > -1) + return &mWeatherSettings[mNextWeather]; + return nullptr; + } + int getNextWeatherID() const { return mNextWeather; } float getTransitionFactor() const { return mTransitionFactor; } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index f608b8c781..c07f5b9161 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -517,13 +517,6 @@ namespace MWWorld mStore.checkPlayer(); mPlayer->readRecord(reader, type); - if (getPlayerPtr().isInCell()) - { - if (getPlayerPtr().getCell()->isExterior()) - mWorldScene->preloadTerrain(getPlayerPtr().getRefData().getPosition().asVec3(), - getPlayerPtr().getCell()->getCell()->getWorldSpace()); - mWorldScene->preloadCellWithSurroundings(*getPlayerPtr().getCell()); - } break; case ESM::REC_CSTA: // We need to rebuild the ESMStore index in order to be able to lookup dynamic records while loading the @@ -1875,14 +1868,43 @@ namespace MWWorld return ESM::Cell::sDefaultWorldspaceId; } - int World::getCurrentWeather() const + const std::vector& World::getAllWeather() const { - return mWeatherManager->getWeatherID(); + return mWeatherManager->getAllWeather(); } - int World::getNextWeather() const + int World::getCurrentWeatherScriptId() const { - return mWeatherManager->getNextWeatherID(); + return mWeatherManager->getWeather().mScriptId; + } + + const MWWorld::Weather& World::getCurrentWeather() const + { + return mWeatherManager->getWeather(); + } + + const MWWorld::Weather* World::getWeather(size_t index) const + { + return mWeatherManager->getWeather(index); + } + + const MWWorld::Weather* World::getWeather(const ESM::RefId& id) const + { + return mWeatherManager->getWeather(id); + } + + int World::getNextWeatherScriptId() const + { + auto next = mWeatherManager->getNextWeather(); + if (next == nullptr) + return -1; + + return next->mScriptId; + } + + const MWWorld::Weather* World::getNextWeather() const + { + return mWeatherManager->getNextWeather(); } float World::getWeatherTransition() const @@ -1900,6 +1922,11 @@ namespace MWWorld mWeatherManager->changeWeather(region, id); } + void World::changeWeather(const ESM::RefId& region, const ESM::RefId& id) + { + mWeatherManager->changeWeather(region, id); + } + void World::modRegion(const ESM::RefId& regionid, const std::vector& chances) { mWeatherManager->modRegion(regionid, chances); @@ -3140,6 +3167,11 @@ namespace MWWorld } } + const osg::Vec4f& World::getSunLightPosition() const + { + return mRendering->getSunLightPosition(); + } + float World::getSunVisibility() const { return mWeatherManager->getSunVisibility(); @@ -3674,24 +3706,6 @@ namespace MWWorld } } - void World::spawnBloodEffect(const Ptr& ptr, const osg::Vec3f& worldPosition) - { - if (ptr == getPlayerPtr() && Settings::gui().mHitFader) - return; - - std::string_view texture - = Fallback::Map::getString("Blood_Texture_" + std::to_string(ptr.getClass().getBloodTexture(ptr))); - if (texture.empty()) - texture = Fallback::Map::getString("Blood_Texture_0"); - - // [0, 2] - const int number = Misc::Rng::rollDice(3); - const VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath( - VFS::Path::Normalized(Fallback::Map::getString("Blood_Model_" + std::to_string(number)))); - - mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false, false); - } - void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight) { @@ -3839,10 +3853,11 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); + const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents; + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent); } 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 b1286d5532..16f91177a1 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -317,9 +317,16 @@ namespace MWWorld void changeWeather(const ESM::RefId& region, const unsigned int id) override; - int getCurrentWeather() const override; + void changeWeather(const ESM::RefId& region, const ESM::RefId& id) override; - int getNextWeather() const override; + const std::vector& getAllWeather() const override; + + int getCurrentWeatherScriptId() const override; + const MWWorld::Weather& getCurrentWeather() const override; + const MWWorld::Weather* getWeather(size_t index) const override; + const MWWorld::Weather* getWeather(const ESM::RefId& id) const override; + int getNextWeatherScriptId() const override; + const MWWorld::Weather* getNextWeather() const override; float getWeatherTransition() const override; @@ -573,6 +580,7 @@ namespace MWWorld // Allow NPCs to use torches? bool useTorches() const override; + const osg::Vec4f& getSunLightPosition() const override; float getSunVisibility() const override; float getSunPercentage() const override; @@ -600,9 +608,6 @@ namespace MWWorld /// Spawn a random creature from a levelled list next to the player void spawnRandomCreature(const ESM::RefId& creatureList) override; - /// Spawn a blood effect for \a ptr at \a worldPosition - void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; - void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) override; @@ -656,8 +661,7 @@ 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, - std::span ignore, std::vector* occupyingActors) const override; + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; diff --git a/apps/openmw_tests/mwworld/testptr.cpp b/apps/openmw_tests/mwworld/testptr.cpp index 7bc0bebcec..9d1dd7aa60 100644 --- a/apps/openmw_tests/mwworld/testptr.cpp +++ b/apps/openmw_tests/mwworld/testptr.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace MWWorld @@ -36,7 +37,7 @@ namespace MWWorld LiveCellRef liveCellRef(cellRef, &npc); liveCellRef.mData.setDeletedByContentFile(true); Ptr ptr(&liveCellRef); - EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")"); + EXPECT_THAT(ptr.toString(), StrCaseEq("deleted object0xd00002a (NPC, \"player\")")); } TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr) @@ -53,7 +54,7 @@ namespace MWWorld cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd }; LiveCellRef liveCellRef(cellRef, &npc); Ptr ptr(&liveCellRef); - EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")"); + EXPECT_THAT(ptr.toString(), StrCaseEq("object0xd00002a (NPC, \"player\")")); } TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction) diff --git a/apps/wizard/main.cpp b/apps/wizard/main.cpp index 09a34994e4..bfade9bf68 100644 --- a/apps/wizard/main.cpp +++ b/apps/wizard/main.cpp @@ -45,7 +45,7 @@ int main(int argc, char* argv[]) resourcesPath = Files::pathToQString(variables["resources"].as().u8string()); } - l10n::installQtTranslations(app, "wizard", resourcesPath); + L10n::installQtTranslations(app, "wizard", resourcesPath); Wizard::MainWizard wizard(std::move(configurationManager)); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index aa6c8763bb..8a3325ea71 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -61,6 +61,7 @@ add_component_dir (lua luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8 shapes/box inputactions yamlloader scripttracker luastateptr ) +copy_resource_file("lua/util.lua" "${OPENMW_RESOURCES_ROOT}" "resources/lua_libs/util.lua") add_component_dir (l10n messagebundles manager @@ -128,7 +129,7 @@ add_component_dir (vfs add_component_dir (resource scenemanager keyframemanager imagemanager animblendrulesmanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem - resourcemanager stats animation foreachbulletobject errormarker cachestats bgsmfilemanager + resourcemanager stats animation foreachbulletobject errormarker selectionmarker cachestats bgsmfilemanager ) add_component_dir (shader diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index a438121d5b..afb2c0a4aa 100644 --- a/components/bsa/ba2dx10file.cpp +++ b/components/bsa/ba2dx10file.cpp @@ -1,29 +1,12 @@ #include "ba2dx10file.hpp" +#include #include +#include #include #include -#include - -#if defined(_MSC_VER) -// why is this necessary? These are included with /external:I -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#include -#endif - -#include +#include #include #include @@ -644,11 +627,16 @@ namespace Bsa size_t headerSize = (header.ddspf.fourCC == ESM::fourCC("DX10") ? sizeof(DDSHeaderDX10) : sizeof(DDSHeader)); size_t textureSize = sizeof(uint32_t) + headerSize; //"DDS " + header + uint32_t maxPackedChunkSize = 0; for (const auto& textureChunk : fileRecord.texturesChunks) + { textureSize += textureChunk.size; + maxPackedChunkSize = std::max(textureChunk.packedSize, maxPackedChunkSize); + } auto memoryStreamPtr = std::make_unique(textureSize); char* buff = memoryStreamPtr->getRawData(); + std::vector inputBuffer(maxPackedChunkSize); uint32_t dds = ESM::fourCC("DDS "); buff = (char*)std::memcpy(buff, &dds, sizeof(uint32_t)) + sizeof(uint32_t); @@ -658,25 +646,22 @@ namespace Bsa // append chunks for (const auto& c : fileRecord.texturesChunks) { + const uint32_t inputSize = c.packedSize != 0 ? c.packedSize : c.size; + Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, inputSize); if (c.packedSize != 0) { - Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, c.packedSize); - std::istream* fileStream = streamPtr.get(); + streamPtr->read(inputBuffer.data(), c.packedSize); + uLongf destSize = static_cast(c.size); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData() + offset), &destSize, + reinterpret_cast(inputBuffer.data()), static_cast(c.packedSize)); - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*fileStream); - - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData() + offset, c.size); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + fail("zlib uncompress failed: " + std::string(::zError(ec))); } // uncompressed chunk else { - Files::IStreamPtr streamPtr = Files::openConstrainedFileStream(mFilepath, c.offset, c.size); - std::istream* fileStream = streamPtr.get(); - - fileStream->read(memoryStreamPtr->getRawData() + offset, c.size); + streamPtr->read(memoryStreamPtr->getRawData() + offset, c.size); } offset += c.size; } diff --git a/components/bsa/ba2dx10file.hpp b/components/bsa/ba2dx10file.hpp index cd1f822179..c902a4ccb0 100644 --- a/components/bsa/ba2dx10file.hpp +++ b/components/bsa/ba2dx10file.hpp @@ -50,6 +50,7 @@ namespace Bsa public: using BSAFile::getFilename; using BSAFile::getList; + using BSAFile::getPath; using BSAFile::open; BA2DX10File(); diff --git a/components/bsa/ba2gnrlfile.cpp b/components/bsa/ba2gnrlfile.cpp index 75e7305245..f169440208 100644 --- a/components/bsa/ba2gnrlfile.cpp +++ b/components/bsa/ba2gnrlfile.cpp @@ -1,27 +1,11 @@ #include "ba2gnrlfile.hpp" +#include #include #include #include -#include - -#if defined(_MSC_VER) -// why is this necessary? These are included with /external:I -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#endif - -#include +#include #include #include @@ -223,12 +207,14 @@ namespace Bsa auto memoryStreamPtr = std::make_unique(fileRecord.size); if (fileRecord.packedSize) { - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*streamPtr); + std::vector buffer(inputSize); + streamPtr->read(buffer.data(), inputSize); + uLongf destSize = static_cast(fileRecord.size); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData()), &destSize, + reinterpret_cast(buffer.data()), static_cast(buffer.size())); - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData(), fileRecord.size); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + fail("zlib uncompress failed: " + std::string(::zError(ec))); } else { diff --git a/components/bsa/ba2gnrlfile.hpp b/components/bsa/ba2gnrlfile.hpp index 0bc94eae0e..080ba3a8df 100644 --- a/components/bsa/ba2gnrlfile.hpp +++ b/components/bsa/ba2gnrlfile.hpp @@ -38,6 +38,7 @@ namespace Bsa public: using BSAFile::getFilename; using BSAFile::getList; + using BSAFile::getPath; using BSAFile::open; BA2GNRLFile(); diff --git a/components/bsa/bsafile.hpp b/components/bsa/bsafile.hpp index ad7acdad17..7b910208d8 100644 --- a/components/bsa/bsafile.hpp +++ b/components/bsa/bsafile.hpp @@ -84,15 +84,15 @@ namespace Bsa protected: bool mHasChanged = false; + /// True when an archive has been loaded + bool mIsLoaded = false; + /// Table of files in this archive FileList mFiles; /// Filename string buffer std::vector mStringBuf; - /// True when an archive has been loaded - bool mIsLoaded; - /// Used for error messages std::filesystem::path mFilepath; @@ -109,11 +109,6 @@ namespace Bsa * ----------------------------------- */ - BSAFile() - : mIsLoaded(false) - { - } - virtual ~BSAFile() { close(); @@ -148,6 +143,11 @@ namespace Bsa return Files::pathToUnicodeString(mFilepath); } + const std::filesystem::path& getPath() const + { + return mFilepath; + } + // checks version of BSA from file header static BsaVersion detectVersion(const std::filesystem::path& filePath); }; diff --git a/components/bsa/compressedbsafile.cpp b/components/bsa/compressedbsafile.cpp index 8426c5965c..655a4d2844 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -24,27 +24,13 @@ */ #include "compressedbsafile.hpp" +#include #include #include #include #include - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable : 4706) -#pragma warning(disable : 4702) -#include -#include -#include -#pragma warning(pop) -#else -#include -#include -#include -#endif - -#include +#include #include #include @@ -292,19 +278,26 @@ namespace Bsa if (compressed) { + std::vector buffer(size); + streamPtr->read(buffer.data(), size); + if (mHeader.mVersion != Version_SSE) { - boost::iostreams::filtering_streambuf inputStreamBuf; - inputStreamBuf.push(boost::iostreams::zlib_decompressor()); - inputStreamBuf.push(*streamPtr); + uLongf destSize = static_cast(resultSize); + int ec = ::uncompress(reinterpret_cast(memoryStreamPtr->getRawData()), &destSize, + reinterpret_cast(buffer.data()), static_cast(buffer.size())); - boost::iostreams::basic_array_sink sr(memoryStreamPtr->getRawData(), resultSize); - boost::iostreams::copy(inputStreamBuf, sr); + if (ec != Z_OK) + { + std::string message = "zlib uncompress failed for file "; + message.append(fileRecord.mName.begin(), fileRecord.mName.end()); + message += ": "; + message += ::zError(ec); + fail(message); + } } else { - auto buffer = std::vector(size); - streamPtr->read(buffer.data(), size); LZ4F_decompressionContext_t context = nullptr; LZ4F_createDecompressionContext(&context, LZ4F_VERSION); LZ4F_decompressOptions_t options = {}; diff --git a/components/bsa/compressedbsafile.hpp b/components/bsa/compressedbsafile.hpp index 83620f11bc..1e359ea3fe 100644 --- a/components/bsa/compressedbsafile.hpp +++ b/components/bsa/compressedbsafile.hpp @@ -117,6 +117,7 @@ namespace Bsa public: using BSAFile::getFilename; using BSAFile::getList; + using BSAFile::getPath; using BSAFile::open; CompressedBSAFile() = default; diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index 6f57ee7673..103fb41641 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -163,6 +163,7 @@ namespace Compiler extensions.registerInstruction("journal", "cl", opcodeJournal, opcodeJournalExplicit); extensions.registerInstruction("setjournalindex", "cl", opcodeSetJournalIndex); extensions.registerFunction("getjournalindex", 'l', "c", opcodeGetJournalIndex); + extensions.registerInstruction("filljournal", "", opcodeFillJournal); extensions.registerInstruction("addtopic", "S", opcodeAddTopic); extensions.registerInstruction( "choice", "j/SlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSlSl", opcodeChoice); diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 2ec31c7588..55f431f049 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -155,6 +155,7 @@ namespace Compiler const int opcodeJournalExplicit = 0x200030b; const int opcodeSetJournalIndex = 0x2000134; const int opcodeGetJournalIndex = 0x2000135; + const int opcodeFillJournal = 0x2000326; const int opcodeAddTopic = 0x200013a; const int opcodeChoice = 0x2000a; const int opcodeForceGreeting = 0x200014f; diff --git a/components/config/gamesettings.cpp b/components/config/gamesettings.cpp index 7f022bb653..f5d9918c10 100644 --- a/components/config/gamesettings.cpp +++ b/components/config/gamesettings.cpp @@ -25,11 +25,6 @@ namespace Config::GameSettings::GameSettings(const Files::ConfigurationManager& cfg) : mCfgMgr(cfg) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - // this needs calling once so Qt can see its stream operators, which it needs when dragging and dropping - // it's automatic with Qt 6 - qRegisterMetaTypeStreamOperators("Config::SettingValue"); -#endif } void Config::GameSettings::validatePaths() diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index e5efa8815f..d01b6bc1c7 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -13,7 +13,6 @@ #include -#include #include #include #include @@ -64,6 +63,25 @@ namespace DetourNavigator std::reference_wrapper mSettings; }; + template + class ToNavMeshCoordinatesSpan + { + public: + explicit ToNavMeshCoordinatesSpan(std::span span, const RecastSettings& settings) + : mSpan(span) + , mSettings(settings) + { + } + + std::size_t size() const noexcept { return mSpan.size(); } + + T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); } + + private: + std::span mSpan; + const RecastSettings& mSettings; + }; + inline std::optional findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, std::span pathBuffer) @@ -79,7 +97,7 @@ namespace DetourNavigator } Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, - std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, + std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst, std::output_iterator auto& out) { assert(polygonPathSize <= polygonPath.size()); @@ -95,7 +113,7 @@ namespace DetourNavigator dtStatusFailed(status)) return Status::FindStraightPathFailed; - for (int i = 0; i < cornersCount; ++i) + for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i) *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast(i) * 3]); return Status::Success; @@ -103,7 +121,8 @@ namespace DetourNavigator Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, - float endTolerance, std::output_iterator auto out) + float endTolerance, const ToNavMeshCoordinatesSpan& checkpoints, + std::output_iterator auto out) { dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); @@ -131,29 +150,66 @@ namespace DetourNavigator return Status::EndPolygonNotFound; std::vector polygonPath(settings.mMaxPolygonPathSize); - const auto polygonPathSize - = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + std::span polygonPathBuffer = polygonPath; + dtPolyRef currentRef = startRef; + osg::Vec3f currentNavMeshPos = startNavMeshPos; + bool skipFirst = false; - if (!polygonPathSize.has_value()) + for (std::size_t i = 0; i < checkpoints.size(); ++i) + { + const osg::Vec3f checkpointPos = checkpoints[i]; + osg::Vec3f checkpointNavMeshPos; + dtPolyRef checkpointRef; + if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(), + &queryFilter, &checkpointRef, checkpointNavMeshPos.ptr()); + dtStatusFailed(status) || checkpointRef == 0) + continue; + + const std::optional toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef, + checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath); + + if (!toCheckpointPathSize.has_value()) + continue; + + if (*toCheckpointPathSize == 0) + continue; + + if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef) + continue; + + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos, + polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out); + + if (smoothStatus != Status::Success) + return smoothStatus; + + currentRef = checkpointRef; + currentNavMeshPos = checkpointNavMeshPos; + skipFirst = true; + } + + const std::optional toEndPathSize = findPolygonPath( + navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer); + + if (!toEndPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (*polygonPathSize == 0) - return Status::Success; + if (*toEndPathSize == 0) + return currentRef == endRef ? Status::Success : Status::PartialPath; osg::Vec3f targetNavMeshPos; if (const dtStatus status = navMeshQuery.closestPointOnPoly( - polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); + polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); dtStatusFailed(status)) return Status::TargetPolygonNotFound; - const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; - const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, - *polygonPathSize, settings.mMaxSmoothPathSize, out); + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath, + *toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out); if (smoothStatus != Status::Success) return smoothStatus; - return partialPath ? Status::PartialPath : Status::Success; + return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath; } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index ca02682ecd..d3b8b5e35a 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -11,6 +11,7 @@ #include #include +#include namespace DetourNavigator { @@ -21,13 +22,13 @@ namespace DetourNavigator * @param end path at given point. * @param includeFlags setup allowed navmesh areas. * @param out the beginning of the destination range. - * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents. + * @param checkpoints is a sequence of positions the path should go over if possible. * @return Status. - * Equal to out if no path is found. */ inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, - std::output_iterator auto out) + std::span checkpoints, std::output_iterator auto out) { const auto navMesh = navigator.getNavMesh(agentBounds); if (navMesh == nullptr) @@ -37,7 +38,8 @@ namespace DetourNavigator const auto locked = navMesh->lock(); return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, - areaCosts, settings.mDetour, endTolerance, outTransform); + areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast), + outTransform); } /** diff --git a/components/esm/records.hpp b/components/esm/records.hpp index 0b60b44cf0..0b76fab0ff 100644 --- a/components/esm/records.hpp +++ b/components/esm/records.hpp @@ -74,6 +74,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/components/esm3/loadinfo.cpp b/components/esm3/loadinfo.cpp index 8b1147ed45..89ba6d8aff 100644 --- a/components/esm3/loadinfo.cpp +++ b/components/esm3/loadinfo.cpp @@ -76,15 +76,15 @@ namespace ESM break; case fourCC("QSTN"): mQuestStatus = QS_Name; - esm.skipRecord(); + esm.skipHSub(); break; case fourCC("QSTF"): mQuestStatus = QS_Finished; - esm.skipRecord(); + esm.skipHSub(); break; case fourCC("QSTR"): mQuestStatus = QS_Restart; - esm.skipRecord(); + esm.skipHSub(); break; case SREC_DELE: esm.skipHSub(); diff --git a/components/esmterrain/gridsampling.hpp b/components/esmterrain/gridsampling.hpp index e71dfc5152..86544a214b 100644 --- a/components/esmterrain/gridsampling.hpp +++ b/components/esmterrain/gridsampling.hpp @@ -15,7 +15,9 @@ namespace ESMTerrain inline std::pair toCellAndLocal( std::size_t begin, std::size_t global, std::size_t cellSize) { + // NOLINTBEGIN(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t cell = global / (cellSize - 1); + // NOLINTEND(clang-analyzer-core.UndefinedBinaryOperatorResult) std::size_t local = global & (cellSize - 2); if (global != begin && local == 0) { diff --git a/components/fallback/validate.hpp b/components/fallback/validate.hpp index 9540c85654..f5dfff9e26 100644 --- a/components/fallback/validate.hpp +++ b/components/fallback/validate.hpp @@ -5,10 +5,12 @@ #include #include +// NOLINTBEGIN(readability-identifier-naming) namespace boost { class any; } +// NOLINTEND(readability-identifier-naming) namespace Fallback { diff --git a/components/files/configurationmanager.cpp b/components/files/configurationmanager.cpp index 7b4cbac864..49fdd996a7 100644 --- a/components/files/configurationmanager.cpp +++ b/components/files/configurationmanager.cpp @@ -18,37 +18,33 @@ namespace Files namespace bpo = boost::program_options; + namespace + { #if defined(_WIN32) || defined(__WINDOWS__) - static const char* const applicationName = "OpenMW"; + constexpr auto applicationName = "OpenMW"; #else - static const char* const applicationName = "openmw"; + constexpr auto applicationName = "openmw"; #endif - static constexpr auto localToken = u8"?local?"; - static constexpr auto userConfigToken = u8"?userconfig?"; - static constexpr auto userDataToken = u8"?userdata?"; - static constexpr auto globalToken = u8"?global?"; + using GetPath = const std::filesystem::path& (Files::FixedPath<>::*)() const; + constexpr std::array, 4> sTokenMappings = { + std::make_pair(u8"?local?", &FixedPath<>::getLocalPath), + std::make_pair(u8"?userconfig?", &FixedPath<>::getUserConfigPath), + std::make_pair(u8"?userdata?", &FixedPath<>::getUserDataPath), + std::make_pair(u8"?global?", &FixedPath<>::getGlobalDataPath), + }; + } ConfigurationManager::ConfigurationManager(bool silent) : mFixedPath(applicationName) , mSilent(silent) { - setupTokensMapping(); - // Initialize with fixed paths, will be overridden in `readConfiguration`. mUserDataPath = mFixedPath.getUserDataPath(); mScreenshotPath = mFixedPath.getUserDataPath() / "screenshots"; } - ConfigurationManager::~ConfigurationManager() {} - - void ConfigurationManager::setupTokensMapping() - { - mTokensMapping.insert(std::make_pair(localToken, &FixedPath<>::getLocalPath)); - mTokensMapping.insert(std::make_pair(userConfigToken, &FixedPath<>::getUserConfigPath)); - mTokensMapping.insert(std::make_pair(userDataToken, &FixedPath<>::getUserDataPath)); - mTokensMapping.insert(std::make_pair(globalToken, &FixedPath<>::getGlobalDataPath)); - } + ConfigurationManager::~ConfigurationManager() = default; static bool hasReplaceConfig(const bpo::variables_map& variables) { @@ -74,7 +70,7 @@ namespace Files std::optional config = loadConfig(mFixedPath.getLocalPath(), description); if (config) mActiveConfigPaths.push_back(mFixedPath.getLocalPath()); - else + else if (!mFixedPath.getGlobalConfigPath().empty()) { mActiveConfigPaths.push_back(mFixedPath.getGlobalConfigPath()); config = loadConfig(mFixedPath.getGlobalConfigPath(), description); @@ -305,15 +301,18 @@ namespace Files const auto pos = str.find('?', 1); if (pos != std::u8string::npos && pos != 0) { - auto tokenIt = mTokensMapping.find(str.substr(0, pos + 1)); - if (tokenIt != mTokensMapping.end()) + std::u8string_view view(str); + auto token = view.substr(0, pos + 1); + auto found = std::find_if( + sTokenMappings.begin(), sTokenMappings.end(), [&](const auto& item) { return item.first == token; }); + if (found != sTokenMappings.end()) { - auto tempPath(((mFixedPath).*(tokenIt->second))()); - if (pos < str.length() - 1) + auto tempPath(((mFixedPath).*(found->second))()); + if (!tempPath.empty() && pos < view.length() - 1) { // There is something after the token, so we should // append it to the path - tempPath /= str.substr(pos + 1, str.length() - pos); + tempPath /= view.substr(pos + 1, view.length() - pos); } path = std::move(tempPath); diff --git a/components/files/configurationmanager.hpp b/components/files/configurationmanager.hpp index 306ea38fe1..184c6ebb82 100644 --- a/components/files/configurationmanager.hpp +++ b/components/files/configurationmanager.hpp @@ -9,11 +9,13 @@ #include #include +// NOLINTBEGIN(readability-identifier-naming) namespace boost::program_options { class options_description; class variables_map; } +// NOLINTEND(readability-identifier-naming) /** * \namespace Files @@ -60,17 +62,12 @@ namespace Files private: typedef Files::FixedPath<> FixedPathType; - typedef const std::filesystem::path& (FixedPathType::*path_type_f)() const; - typedef std::map TokensMappingContainer; - std::optional loadConfig( const std::filesystem::path& path, const boost::program_options::options_description& description) const; void addExtraConfigDirs( std::stack& dirs, const boost::program_options::variables_map& variables) const; - void setupTokensMapping(); - std::vector mActiveConfigPaths; FixedPathType mFixedPath; @@ -78,8 +75,6 @@ namespace Files std::filesystem::path mUserDataPath; std::filesystem::path mScreenshotPath; - TokensMappingContainer mTokensMapping; - bool mSilent; }; diff --git a/components/files/qtconfigpath.hpp b/components/files/qtconfigpath.hpp index 16e0499cd5..a2154ce110 100644 --- a/components/files/qtconfigpath.hpp +++ b/components/files/qtconfigpath.hpp @@ -8,21 +8,11 @@ namespace Files { - inline QString getLocalConfigPathQString(const Files::ConfigurationManager& cfgMgr) - { - return Files::pathToQString(cfgMgr.getLocalPath() / openmwCfgFile); - } - inline QString getUserConfigPathQString(const Files::ConfigurationManager& cfgMgr) { return Files::pathToQString(cfgMgr.getUserConfigPath() / openmwCfgFile); } - inline QString getGlobalConfigPathQString(const Files::ConfigurationManager& cfgMgr) - { - return Files::pathToQString(cfgMgr.getGlobalPath() / openmwCfgFile); - } - inline QStringList getActiveConfigPathsQString(const Files::ConfigurationManager& cfgMgr) { const auto& activePaths = cfgMgr.getActiveConfigPaths(); diff --git a/components/files/windowspath.cpp b/components/files/windowspath.cpp index 77faa23131..60ac5e265c 100644 --- a/components/files/windowspath.cpp +++ b/components/files/windowspath.cpp @@ -54,19 +54,7 @@ namespace Files { // The concept of a global config path is absurd on Windows. // Always use local config instead. - // The virtual base class requires that we provide this, though. - std::filesystem::path globalPath = std::filesystem::current_path(); - - PWSTR cString; - HRESULT result = SHGetKnownFolderPath(FOLDERID_ProgramFiles, 0, nullptr, &cString); - if (SUCCEEDED(result)) - globalPath = std::filesystem::path(cString); - else - Log(Debug::Error) << "Error " << result << " when getting Program Files path"; - - CoTaskMemFree(cString); - - return globalPath / mName; + return {}; } std::filesystem::path WindowsPath::getLocalPath() const diff --git a/components/files/windowspath.hpp b/components/files/windowspath.hpp index 380e831b20..ed2bbdfc2e 100644 --- a/components/files/windowspath.hpp +++ b/components/files/windowspath.hpp @@ -34,7 +34,7 @@ namespace Files std::filesystem::path getUserDataPath() const; /** - * \brief Returns "X:\Program Files\" + * \brief Returns an empty path * * \return std::filesystem::path */ diff --git a/components/fontloader/fontloader.cpp b/components/fontloader/fontloader.cpp index c9003f3aa8..f43c78bfd3 100644 --- a/components/fontloader/fontloader.cpp +++ b/components/fontloader/fontloader.cpp @@ -276,8 +276,8 @@ namespace Gui { Log(Debug::Info) << "Loading font file " << fileName; - osgMyGUI::DataManager* dataManager - = dynamic_cast(&osgMyGUI::DataManager::getInstance()); + MyGUIPlatform::DataManager* dataManager + = dynamic_cast(&MyGUIPlatform::DataManager::getInstance()); if (!dataManager) { Log(Debug::Error) << "Can not load TrueType font " << fontId << ": osgMyGUI::DataManager is not available."; @@ -287,7 +287,7 @@ namespace Gui // TODO: it may be worth to take in account resolution change, but it is not safe to replace used assets std::unique_ptr layersStream(dataManager->getData("openmw_layers.xml")); MyGUI::IntSize bookSize = getBookSize(layersStream.get()); - float bookScale = osgMyGUI::ScalingLayer::getScaleFactor(bookSize); + float bookScale = MyGUIPlatform::ScalingLayer::getScaleFactor(bookSize); const auto oldDataPath = dataManager->getDataPath({}); dataManager->setResourcePath("fonts"); diff --git a/components/fx/lexer.cpp b/components/fx/lexer.cpp index 2fc25e44f1..cab59df314 100644 --- a/components/fx/lexer.cpp +++ b/components/fx/lexer.cpp @@ -8,7 +8,7 @@ #include -namespace fx +namespace Fx { namespace Lexer { diff --git a/components/fx/lexer.hpp b/components/fx/lexer.hpp index dda6b3a0f6..1b32298608 100644 --- a/components/fx/lexer.hpp +++ b/components/fx/lexer.hpp @@ -9,7 +9,7 @@ #include "lexertypes.hpp" -namespace fx +namespace Fx { namespace Lexer { diff --git a/components/fx/lexertypes.hpp b/components/fx/lexertypes.hpp index 2a56a84a1a..327d6832c8 100644 --- a/components/fx/lexertypes.hpp +++ b/components/fx/lexertypes.hpp @@ -4,7 +4,7 @@ #include #include -namespace fx +namespace Fx { namespace Lexer { diff --git a/components/fx/parseconstants.hpp b/components/fx/parseconstants.hpp index 3ad9abd959..eec315042b 100644 --- a/components/fx/parseconstants.hpp +++ b/components/fx/parseconstants.hpp @@ -13,11 +13,11 @@ #include "technique.hpp" -namespace fx +namespace Fx { - namespace constants + namespace Constants { - constexpr std::array, 6> TechniqueFlag = { { + constexpr std::array, 6> TechniqueFlag = { { { "disable_interiors", Technique::Flag_Disable_Interiors }, { "disable_exteriors", Technique::Flag_Disable_Exteriors }, { "disable_underwater", Technique::Flag_Disable_Underwater }, diff --git a/components/fx/pass.cpp b/components/fx/pass.cpp index c55bee76e3..85b420f4fd 100644 --- a/components/fx/pass.cpp +++ b/components/fx/pass.cpp @@ -49,7 +49,7 @@ void main() } -namespace fx +namespace Fx { Pass::Pass(Pass::Type type, Pass::Order order, bool ubo) : mCompiled(false) diff --git a/components/fx/pass.hpp b/components/fx/pass.hpp index 1c417ac8cf..2e68bddcc5 100644 --- a/components/fx/pass.hpp +++ b/components/fx/pass.hpp @@ -18,7 +18,7 @@ namespace osg class StateSet; } -namespace fx +namespace Fx { class Technique; diff --git a/components/fx/stateupdater.cpp b/components/fx/stateupdater.cpp index 9e86f25b9c..8dd6bce994 100644 --- a/components/fx/stateupdater.cpp +++ b/components/fx/stateupdater.cpp @@ -5,7 +5,7 @@ #include -namespace fx +namespace Fx { std::string StateUpdater::sDefinition = UniformData::getDefinition("_omw_data"); diff --git a/components/fx/stateupdater.hpp b/components/fx/stateupdater.hpp index 33a7a09fe6..f5921faadf 100644 --- a/components/fx/stateupdater.hpp +++ b/components/fx/stateupdater.hpp @@ -7,7 +7,7 @@ #include #include -namespace fx +namespace Fx { class StateUpdater : public SceneUtil::StateSetUpdater { diff --git a/components/fx/technique.cpp b/components/fx/technique.cpp index a8cd455dea..a88a1a62bb 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -35,24 +35,22 @@ namespace }; } -namespace fx +namespace Fx { - namespace + VFS::Path::Normalized Technique::makeFileName(std::string_view name) { - VFS::Path::Normalized makeFilePath(std::string_view name) - { - std::string fileName(name); - fileName += Technique::sExt; - VFS::Path::Normalized result(Technique::sSubdir); - result /= fileName; - return result; - } + std::string fileName(name); + fileName += '.'; + fileName += Technique::sExt; + VFS::Path::Normalized result(Technique::sSubdir); + result /= fileName; + return result; } - Technique::Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, std::string name, int width, - int height, bool ubo, bool supportsNormals) + Technique::Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, + VFS::Path::NormalizedView fileName, std::string name, int width, int height, bool ubo, bool supportsNormals) : mName(std::move(name)) - , mFilePath(makeFilePath(mName)) + , mFilePath(fileName) , mLastModificationTime(std::filesystem::file_time_type::clock::now()) , mWidth(width) , mHeight(height) @@ -282,7 +280,7 @@ namespace fx if (mRenderTargets.count(mBlockName)) error(Misc::StringUtils::format("redeclaration of render target '%s'", std::string(mBlockName))); - fx::Types::RenderTarget rt; + Fx::Types::RenderTarget rt; rt.mTarget->setTextureSize(mWidth, mHeight); rt.mTarget->setSourceFormat(GL_RGB); rt.mTarget->setInternalFormat(GL_RGB); @@ -343,7 +341,7 @@ namespace fx auto& pass = mPassMap[mBlockName]; if (!pass) - pass = std::make_shared(); + pass = std::make_shared(); pass->mName = mBlockName; @@ -366,7 +364,7 @@ namespace fx auto& pass = mPassMap[mBlockName]; if (!pass) - pass = std::make_shared(); + pass = std::make_shared(); pass->mUBO = mUBO; pass->mName = mBlockName; @@ -390,7 +388,7 @@ namespace fx auto& pass = mPassMap[mBlockName]; if (!pass) - pass = std::make_shared(); + pass = std::make_shared(); pass->mName = mBlockName; @@ -530,8 +528,6 @@ namespace fx { return parseBool(); } - - error(Misc::StringUtils::format("failed setting uniform type")); } template @@ -559,10 +555,12 @@ namespace fx { if constexpr (std::is_same_v) error("bool arrays currently unsupported"); - - int size = parseInteger(); - if (size > 1) - data.mArray = std::vector(size); + else + { + int size = parseInteger(); + if (size > 1) + data.mArray = std::vector(size); + } } else if (key == "min") { @@ -812,7 +810,7 @@ namespace fx auto& pass = mPassMap[mBlockName]; if (!pass) - pass = std::make_shared(); + pass = std::make_shared(); while (!isNext()) { @@ -885,7 +883,7 @@ namespace fx FlagsType Technique::parseFlags() { auto parseBit = [this](std::string_view term) { - for (const auto& [identifer, bit] : constants::TechniqueFlag) + for (const auto& [identifer, bit] : Constants::TechniqueFlag) { if (Misc::StringUtils::ciEqual(term, identifer)) return bit; @@ -904,7 +902,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::FilterMode) + for (const auto& [identifer, mode] : Constants::FilterMode) { if (asLiteral() == identifer) return mode; @@ -917,7 +915,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::WrapMode) + for (const auto& [identifer, mode] : Constants::WrapMode) { if (asLiteral() == identifer) return mode; @@ -935,7 +933,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::Compression) + for (const auto& [identifer, mode] : Constants::Compression) { if (asLiteral() == identifer) return mode; @@ -948,7 +946,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::InternalFormat) + for (const auto& [identifer, mode] : Constants::InternalFormat) { if (asLiteral() == identifer) return mode; @@ -961,7 +959,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::SourceType) + for (const auto& [identifer, mode] : Constants::SourceType) { if (asLiteral() == identifer) return mode; @@ -974,7 +972,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::SourceFormat) + for (const auto& [identifer, mode] : Constants::SourceFormat) { if (asLiteral() == identifer) return mode; @@ -987,7 +985,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::BlendEquation) + for (const auto& [identifer, mode] : Constants::BlendEquation) { if (asLiteral() == identifer) return mode; @@ -1000,7 +998,7 @@ namespace fx { expect(); - for (const auto& [identifer, mode] : constants::BlendFunc) + for (const auto& [identifer, mode] : Constants::BlendFunc) { if (asLiteral() == identifer) return mode; @@ -1027,11 +1025,11 @@ namespace fx */ expect(); - std::vector> choices; + std::vector> choices; while (!isNext()) { - fx::Types::Choice choice; + Fx::Types::Choice choice; choice.mLabel = parseString(); expect(); choice.mValue = getUniformValue(); diff --git a/components/fx/technique.hpp b/components/fx/technique.hpp index ad5e876faa..ebf3fe30f5 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -29,7 +29,7 @@ namespace VFS class Manager; } -namespace fx +namespace Fx { using FlagsType = size_t; @@ -85,7 +85,7 @@ namespace fx } // not safe to read/write in draw thread - std::shared_ptr mHandle = nullptr; + std::shared_ptr mHandle = nullptr; FlagsType mFlags = 0; @@ -105,8 +105,8 @@ namespace fx using UniformMap = std::vector>; using RenderTargetMap = std::unordered_map; - static constexpr std::string_view sExt = ".omwfx"; - static constexpr std::string_view sSubdir = "shaders"; + static constexpr std::string_view sExt = "omwfx"; + static constexpr VFS::Path::NormalizedView sSubdir{ "shaders" }; enum class Status { @@ -123,8 +123,10 @@ namespace fx static constexpr FlagsType Flag_Disable_SunGlare = (1 << 4); static constexpr FlagsType Flag_Hidden = (1 << 5); - Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, std::string name, int width, - int height, bool ubo, bool supportsNormals); + static VFS::Path::Normalized makeFileName(std::string_view name); + + Technique(const VFS::Manager& vfs, Resource::ImageManager& imageManager, VFS::Path::NormalizedView fileName, + std::string name, int width, int height, bool ubo, bool supportsNormals); bool compile(); diff --git a/components/fx/types.hpp b/components/fx/types.hpp index 1536cda115..440bc69470 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -12,7 +12,7 @@ #include #include -namespace fx +namespace Fx { namespace Types { @@ -220,10 +220,11 @@ namespace fx return osg::Uniform::FLOAT; else if constexpr (std::is_same_v) return osg::Uniform::INT; - else if constexpr (std::is_same_v) + else + { + static_assert(std::is_same_v, "Non-exhaustive visitor"); return osg::Uniform::BOOL; - - return std::nullopt; + } }, mData); } @@ -293,15 +294,14 @@ namespace fx return Misc::StringUtils::format("const int %s=%i;", mName, value); } - else if constexpr (std::is_same_v) + else { + static_assert(std::is_same_v, "Non-exhaustive visitor"); if (useUniform) return Misc::StringUtils::format("uniform bool %s;", uname); return Misc::StringUtils::format("const bool %s=%s;", mName, value ? "true" : "false"); } - - return std::nullopt; }, mData); } diff --git a/components/fx/widgets.cpp b/components/fx/widgets.cpp index 8382ca2d56..087e071b96 100644 --- a/components/fx/widgets.cpp +++ b/components/fx/widgets.cpp @@ -6,7 +6,7 @@ namespace { template void createVectorWidget( - const std::shared_ptr& uniform, MyGUI::Widget* client, fx::Widgets::UniformBase* base) + const std::shared_ptr& uniform, MyGUI::Widget* client, Fx::Widgets::UniformBase* base) { int height = client->getHeight(); base->setSize(base->getSize().width, (base->getSize().height - height) + (height * T::num_components)); @@ -16,13 +16,13 @@ namespace { auto* widget = client->createWidget( "MW_ValueEditNumber", { 0, height * i, client->getWidth(), height }, MyGUI::Align::Default); - widget->setData(uniform, static_cast(i)); + widget->setData(uniform, static_cast(i)); base->addItem(widget); } } } -namespace fx +namespace Fx { namespace Widgets { @@ -127,7 +127,7 @@ namespace fx mChoices->eventComboChangePosition += MyGUI::newDelegate(this, &EditChoice::notifyComboBoxChanged); } - void UniformBase::init(const std::shared_ptr& uniform) + void UniformBase::init(const std::shared_ptr& uniform) { if (uniform->mDisplayName.empty()) mLabel->setCaption(uniform->mName); diff --git a/components/fx/widgets.hpp b/components/fx/widgets.hpp index 59787ed9aa..c91fa01c4e 100644 --- a/components/fx/widgets.hpp +++ b/components/fx/widgets.hpp @@ -28,7 +28,7 @@ namespace Gui class AutoSizedButton; } -namespace fx +namespace Fx { namespace Widgets { @@ -46,7 +46,7 @@ namespace fx public: virtual ~EditBase() = default; - void setData(const std::shared_ptr& uniform, Index index = None) + void setData(const std::shared_ptr& uniform, Index index = None) { mUniform = uniform; mIndex = index; @@ -57,7 +57,7 @@ namespace fx virtual void toDefault() = 0; protected: - std::shared_ptr mUniform; + std::shared_ptr mUniform; Index mIndex; }; @@ -268,7 +268,7 @@ namespace fx MYGUI_RTTI_DERIVED(UniformBase) public: - void init(const std::shared_ptr& uniform); + void init(const std::shared_ptr& uniform); void toDefault(); diff --git a/components/l10n/manager.cpp b/components/l10n/manager.cpp index f6f4bb4f05..1f75c5b073 100644 --- a/components/l10n/manager.cpp +++ b/components/l10n/manager.cpp @@ -6,7 +6,7 @@ #include #include -namespace l10n +namespace L10n { void Manager::setPreferredLocales(const std::vector& langs, bool gmstHasPriority) @@ -14,7 +14,7 @@ namespace l10n mPreferredLocales.clear(); if (gmstHasPriority) mPreferredLocales.push_back(icu::Locale("gmst")); - std::set langSet; + std::set> langSet; for (const auto& lang : langs) { if (langSet.contains(lang)) @@ -31,10 +31,10 @@ namespace l10n msg << " " << l.getName(); } for (auto& [key, context] : mCache) - updateContext(key.first, *context); + updateContext(std::get<0>(key), *context); } - void Manager::readLangData(const std::string& name, MessageBundles& ctx, const icu::Locale& lang) + void Manager::readLangData(std::string_view name, MessageBundles& ctx, const icu::Locale& lang) { std::string langName(lang.getName()); langName += ".yaml"; @@ -58,7 +58,7 @@ namespace l10n } } - void Manager::updateContext(const std::string& name, MessageBundles& ctx) + void Manager::updateContext(std::string_view name, MessageBundles& ctx) { icu::Locale fallbackLocale = ctx.getFallbackLocale(); ctx.setPreferredLocales(mPreferredLocales); @@ -89,9 +89,9 @@ namespace l10n } std::shared_ptr Manager::getContext( - const std::string& contextName, const std::string& fallbackLocaleName) + std::string_view contextName, const std::string& fallbackLocaleName) { - std::pair key(contextName, fallbackLocaleName); + std::tuple key(contextName, fallbackLocaleName); auto it = mCache.find(key); if (it != mCache.end()) return it->second; @@ -102,7 +102,7 @@ namespace l10n for (char c : contextName) valid = valid && allowedChar(c); if (!valid) - throw std::runtime_error(std::string("Invalid l10n context name: ") + contextName); + throw std::runtime_error("Invalid l10n context name: " + std::string(contextName)); icu::Locale fallbackLocale(fallbackLocaleName.c_str()); std::shared_ptr ctx = std::make_shared(mPreferredLocales, fallbackLocale); ctx->setGmstLoader(mGmstLoader); diff --git a/components/l10n/manager.hpp b/components/l10n/manager.hpp index 2ee54921b3..89a9bd4b05 100644 --- a/components/l10n/manager.hpp +++ b/components/l10n/manager.hpp @@ -10,7 +10,7 @@ namespace VFS class Manager; } -namespace l10n +namespace L10n { class Manager @@ -27,20 +27,20 @@ namespace l10n void setGmstLoader(std::function fn) { mGmstLoader = std::move(fn); } std::shared_ptr getContext( - const std::string& contextName, const std::string& fallbackLocale = "en"); + std::string_view contextName, const std::string& fallbackLocale = "en"); - std::string getMessage(const std::string& contextName, std::string_view key) + std::string getMessage(std::string_view contextName, std::string_view key) { return getContext(contextName)->formatMessage(key, {}, {}); } private: - void readLangData(const std::string& name, MessageBundles& ctx, const icu::Locale& lang); - void updateContext(const std::string& name, MessageBundles& ctx); + void readLangData(std::string_view name, MessageBundles& ctx, const icu::Locale& lang); + void updateContext(std::string_view name, MessageBundles& ctx); const VFS::Manager* mVFS; std::vector mPreferredLocales; - std::map, std::shared_ptr> mCache; + std::map, std::shared_ptr, std::less<>> mCache; std::function mGmstLoader; }; diff --git a/components/l10n/messagebundles.cpp b/components/l10n/messagebundles.cpp index 2948ff155e..665704e30d 100644 --- a/components/l10n/messagebundles.cpp +++ b/components/l10n/messagebundles.cpp @@ -7,8 +7,52 @@ #include -namespace l10n +namespace L10n { + namespace + { + std::string getErrorText(const UParseError& parseError) + { + icu::UnicodeString preContext(parseError.preContext), postContext(parseError.postContext); + std::string parseErrorString; + preContext.toUTF8String(parseErrorString); + postContext.toUTF8String(parseErrorString); + return parseErrorString; + } + + template + bool checkSuccess(const icu::ErrorCode& status, const UParseError& parseError, Args const&... message) + { + if (status.isFailure()) + { + std::string errorText = getErrorText(parseError); + if (!errorText.empty()) + { + (Log(Debug::Error) << ... << message) + << ": " << status.errorName() << " in \"" << errorText << "\""; + } + else + { + (Log(Debug::Error) << ... << message) << ": " << status.errorName(); + } + } + return status.isSuccess(); + } + + std::string loadGmst( + const std::function& gmstLoader, const icu::MessageFormat* message) + { + icu::UnicodeString gmstNameUnicode; + std::string gmstName; + icu::ErrorCode success; + message->format(nullptr, nullptr, 0, gmstNameUnicode, success); + gmstNameUnicode.toUTF8String(gmstName); + if (gmstLoader) + return gmstLoader(gmstName); + return "GMST:" + gmstName; + } + } + MessageBundles::MessageBundles(const std::vector& preferredLocales, icu::Locale& fallbackLocale) : mFallbackLocale(fallbackLocale) { @@ -39,33 +83,6 @@ namespace l10n } } - std::string getErrorText(const UParseError& parseError) - { - icu::UnicodeString preContext(parseError.preContext), postContext(parseError.postContext); - std::string parseErrorString; - preContext.toUTF8String(parseErrorString); - postContext.toUTF8String(parseErrorString); - return parseErrorString; - } - - static bool checkSuccess( - const icu::ErrorCode& status, const std::string& message, const UParseError parseError = UParseError()) - { - if (status.isFailure()) - { - std::string errorText = getErrorText(parseError); - if (!errorText.empty()) - { - Log(Debug::Error) << message << ": " << status.errorName() << " in \"" << errorText << "\""; - } - else - { - Log(Debug::Error) << message << ": " << status.errorName(); - } - } - return status.isSuccess(); - } - void MessageBundles::load(std::istream& input, const icu::Locale& lang) { YAML::Node data = YAML::Load(input); @@ -80,20 +97,19 @@ namespace l10n icu::ErrorCode status; UParseError parseError; icu::MessageFormat message(pattern, langOrEn, parseError, status); - if (checkSuccess(status, std::string("Failed to create message ") + key + " for locale " + lang.getName(), - parseError)) + if (checkSuccess(status, parseError, "Failed to create message ", key, " for locale ", lang.getName())) { - mBundles[localeName].insert(std::make_pair(key, message)); + mBundles[localeName].emplace(key, message); } } } - const icu::MessageFormat* MessageBundles::findMessage(std::string_view key, const std::string& localeName) const + const icu::MessageFormat* MessageBundles::findMessage(std::string_view key, std::string_view localeName) const { auto iter = mBundles.find(localeName); if (iter != mBundles.end()) { - auto message = iter->second.find(key.data()); + auto message = iter->second.find(key); if (message != iter->second.end()) { return &(message->second); @@ -116,20 +132,6 @@ namespace l10n return formatMessage(key, argNames, argValues); } - static std::string loadGmst( - const std::function gmstLoader, const icu::MessageFormat* message) - { - icu::UnicodeString gmstNameUnicode; - std::string gmstName; - icu::ErrorCode success; - message->format(nullptr, nullptr, 0, gmstNameUnicode, success); - gmstNameUnicode.toUTF8String(gmstName); - if (gmstLoader) - return gmstLoader(gmstName); - else - return "GMST:" + gmstName; - } - std::string MessageBundles::formatMessage(std::string_view key, const std::vector& argNames, const std::vector& args) const { @@ -158,7 +160,7 @@ namespace l10n message->format(argNames.data(), args.data(), static_cast(args.size()), result, success); else message->format(nullptr, nullptr, static_cast(args.size()), result, success); - checkSuccess(success, std::string("Failed to format message ") + key.data()); + checkSuccess(success, {}, "Failed to format message ", key); result.toUTF8String(resultString); return resultString; } @@ -171,7 +173,7 @@ namespace l10n icu::MessageFormat defaultMessage( icu::UnicodeString::fromUTF8(icu::StringPiece(key.data(), static_cast(key.size()))), defaultLocale, parseError, success); - if (!checkSuccess(success, std::string("Failed to create message ") + key.data(), parseError)) + if (!checkSuccess(success, parseError, "Failed to create message ", key)) // If we can't parse the key as a pattern, just return the key return std::string(key); @@ -180,7 +182,7 @@ namespace l10n argNames.data(), args.data(), static_cast(args.size()), result, success); else defaultMessage.format(nullptr, nullptr, static_cast(args.size()), result, success); - checkSuccess(success, std::string("Failed to format message ") + key.data()); + checkSuccess(success, {}, "Failed to format message ", key); result.toUTF8String(resultString); return resultString; } diff --git a/components/l10n/messagebundles.hpp b/components/l10n/messagebundles.hpp index 0ea92e93fe..15ccbaa630 100644 --- a/components/l10n/messagebundles.hpp +++ b/components/l10n/messagebundles.hpp @@ -10,7 +10,9 @@ #include #include -namespace l10n +#include + +namespace L10n { /** * @brief A collection of Message Bundles @@ -41,18 +43,23 @@ namespace l10n void setPreferredLocales(const std::vector& preferredLocales); const std::vector& getPreferredLocales() const { return mPreferredLocales; } void load(std::istream& input, const icu::Locale& lang); - bool isLoaded(const icu::Locale& loc) const { return mBundles.find(loc.getName()) != mBundles.end(); } + bool isLoaded(const icu::Locale& loc) const + { + return mBundles.find(std::string_view(loc.getName())) != mBundles.end(); + } const icu::Locale& getFallbackLocale() const { return mFallbackLocale; } void setGmstLoader(std::function fn) { mGmstLoader = std::move(fn); } private: + template + using StringMap = std::unordered_map>; // icu::Locale isn't hashable (or comparable), so we use the string form instead, which is canonicalized - std::unordered_map> mBundles; + StringMap> mBundles; const icu::Locale mFallbackLocale; std::vector mPreferredLocaleStrings; std::vector mPreferredLocales; std::function mGmstLoader; - const icu::MessageFormat* findMessage(std::string_view key, const std::string& localeName) const; + const icu::MessageFormat* findMessage(std::string_view key, std::string_view localeName) const; }; } diff --git a/components/l10n/qttranslations.cpp b/components/l10n/qttranslations.cpp index 9bc146699e..fbc38dc122 100644 --- a/components/l10n/qttranslations.cpp +++ b/components/l10n/qttranslations.cpp @@ -3,7 +3,7 @@ #include #include -namespace l10n +namespace L10n { QTranslator AppTranslator{}; QTranslator ComponentsTranslator{}; @@ -14,11 +14,7 @@ namespace l10n // Try to load OpenMW translations from resources folder first. // If we loaded them, try to load Qt translations from both // resources folder and default translations folder as well. -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto qtPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); -#else - auto qtPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath); -#endif auto localPath = resourcesPath + "/translations"; if (AppTranslator.load(QLocale::system(), appName, "_", localPath) diff --git a/components/l10n/qttranslations.hpp b/components/l10n/qttranslations.hpp index 3ce87f0837..6a2bf9d903 100644 --- a/components/l10n/qttranslations.hpp +++ b/components/l10n/qttranslations.hpp @@ -4,7 +4,7 @@ #include #include -namespace l10n +namespace L10n { extern QTranslator AppTranslator; extern QTranslator ComponentsTranslator; diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp index b327270fc1..858137ab73 100644 --- a/components/lua/configuration.cpp +++ b/components/lua/configuration.cpp @@ -55,7 +55,7 @@ namespace LuaUtil // Find duplicates; only the last occurrence will be used (unless `sMerge` flag is used). // Search for duplicates is case insensitive. std::vector skip(cfg.mScripts.size(), false); - for (size_t i = 0; i < cfg.mScripts.size(); ++i) + for (int i = 0; i < static_cast(cfg.mScripts.size()); ++i) { const ESM::LuaScriptCfg& script = cfg.mScripts[i]; bool global = script.mFlags & ESM::LuaScriptCfg::sGlobal; diff --git a/components/lua/l10n.cpp b/components/lua/l10n.cpp index 15177bea65..856ab8a808 100644 --- a/components/lua/l10n.cpp +++ b/components/lua/l10n.cpp @@ -8,7 +8,7 @@ namespace { struct L10nContext { - std::shared_ptr mData; + std::shared_ptr mData; }; void getICUArgs(std::string_view messageId, const sol::table& table, std::vector& argNames, @@ -18,7 +18,11 @@ namespace { // Argument values if (value.is()) - args.push_back(icu::Formattable(LuaUtil::cast(value).c_str())); + { + const auto& str = LuaUtil::cast(value); + args.push_back(icu::Formattable(icu::UnicodeString::fromUTF8(str.c_str()))); + } + // Note: While we pass all numbers as doubles, they still seem to be handled appropriately. // Numbers can be forced to be integers using the argType number and argStyle integer // E.g. {var, number, integer} @@ -48,7 +52,7 @@ namespace sol namespace LuaUtil { - sol::function initL10nLoader(lua_State* L, l10n::Manager* manager) + sol::function initL10nLoader(lua_State* L, L10n::Manager* manager) { sol::state_view lua(L); sol::usertype ctxDef = lua.new_usertype("L10nContext"); @@ -62,7 +66,7 @@ namespace LuaUtil }; return sol::make_object( - lua, [manager](const std::string& contextName, sol::optional fallbackLocale) { + lua, [manager](std::string_view contextName, sol::optional fallbackLocale) { if (fallbackLocale) return L10nContext{ manager->getContext(contextName, *fallbackLocale) }; else diff --git a/components/lua/l10n.hpp b/components/lua/l10n.hpp index 1fc3e17747..2abe7f56f2 100644 --- a/components/lua/l10n.hpp +++ b/components/lua/l10n.hpp @@ -3,14 +3,14 @@ #include -namespace l10n +namespace L10n { class Manager; } namespace LuaUtil { - sol::function initL10nLoader(lua_State*, l10n::Manager* manager); + sol::function initL10nLoader(lua_State*, L10n::Manager* manager); } #endif // COMPONENTS_LUA_L10N_H diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index f959263153..b83415e58d 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -453,7 +453,7 @@ namespace LuaUtil return call(sol::state_view(obj.lua_state())["tostring"], obj); } - std::string internal::formatCastingError(const sol::object& obj, const std::type_info& t) + std::string Internal::formatCastingError(const sol::object& obj, const std::type_info& t) { const char* typeName = t.name(); if (t == typeid(int)) diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index d842478cb1..7ce9a0ec94 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -325,7 +325,7 @@ namespace LuaUtil // String representation of a Lua object. Should be used for debugging/logging purposes only. std::string toString(const sol::object&); - namespace internal + namespace Internal { std::string formatCastingError(const sol::object& obj, const std::type_info&); } @@ -334,7 +334,7 @@ namespace LuaUtil decltype(auto) cast(const sol::object& obj) { if (!obj.is()) - throw std::runtime_error(internal::formatCastingError(obj, typeid(T))); + throw std::runtime_error(Internal::formatCastingError(obj, typeid(T))); return obj.as(); } diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 5eff211894..cdeb528205 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -215,12 +215,13 @@ namespace LuaUtil return true; } } + return false; } - else if constexpr (std::is_same_v) + else { + static_assert(std::is_same_v, "Non-exhaustive visitor"); return variant.mScripts.count(scriptId) != 0; } - return false; }, mData); } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 956b4d4317..dd066d3414 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -165,6 +165,29 @@ namespace LuaUtil virtual bool isActive() const { return false; } protected: + // Call a function on an interface. + template + std::optional callInterface(std::string_view interfaceName, std::string_view identifier, const Args&... args) + { + std::optional res = std::nullopt; + mLua.protectedCall([&](LuaUtil::LuaView& view) { + LoadedData& data = ensureLoaded(); + auto I = data.mPublicInterfaces.get>(interfaceName); + if (I) + { + auto o = I->get_or(identifier, sol::nil); + if (o.is()) + { + sol::object luaRes = o.as().call(args...); + if (luaRes.is()) + res = luaRes.as(); + } + } + }); + + return res; + } + struct Handler { int mScriptId; diff --git a/components/lua/util.lua b/components/lua/util.lua new file mode 100644 index 0000000000..f37a2e52cc --- /dev/null +++ b/components/lua/util.lua @@ -0,0 +1,21 @@ + +local M = {} + +function M.remap(value, min, max, newMin, newMax) + return newMin + (value - min) * (newMax - newMin) / (max - min) +end + +function M.round(value) + return value >= 0 and math.floor(value + 0.5) or math.ceil(value - 0.5) +end + +function M.clamp(value, low, high) + return value < low and low or (value > high and high or value) +end + +function M.normalizeAngle(angle) + local fullTurns = angle / (2 * math.pi) + 0.5 + return (fullTurns - math.floor(fullTurns) - 0.5) * (2 * math.pi) +end + +return M diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 85492ccf06..2b706e1cb8 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -352,16 +352,14 @@ namespace LuaUtil return std::make_tuple(angles.z(), angles.y(), angles.x()); }; + sol::function luaUtilLoader = lua["loadInternalLib"]("util"); + sol::table utils = luaUtilLoader(); + for (const auto& [key, value] : utils) + util[key.as()] = value; + // Utility functions - util["clamp"] = [](double value, double from, double to) { return std::clamp(value, from, to); }; - // NOTE: `util["clamp"] = std::clamp` causes error 'AddressSanitizer: stack-use-after-scope' - util["normalizeAngle"] = &Misc::normalizeAngle; util["makeReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/false); }; util["makeStrictReadOnly"] = [](const sol::table& tbl) { return makeReadOnly(tbl, /*strictIndex=*/true); }; - util["remap"] = [](double value, double min, double max, double newMin, double newMax) { - return newMin + (value - min) * (newMax - newMin) / (max - min); - }; - util["round"] = [](double value) { return round(value); }; if (lua["bit32"] != sol::nil) { diff --git a/components/misc/coordinateconverter.hpp b/components/misc/coordinateconverter.hpp index 7853880809..6c4d8dbf71 100644 --- a/components/misc/coordinateconverter.hpp +++ b/components/misc/coordinateconverter.hpp @@ -59,10 +59,18 @@ namespace Misc point.y() -= static_cast(mCellY); } + osg::Vec3f toWorldVec3(const osg::Vec3f& point) const + { + osg::Vec3f result = point; + toWorld(result); + return result; + } + osg::Vec3f toLocalVec3(const osg::Vec3f& point) const { - return osg::Vec3f( - point.x() - static_cast(mCellX), point.y() - static_cast(mCellY), point.z()); + osg::Vec3f result = point; + toLocal(result); + return result; } private: diff --git a/components/misc/pathgridutils.hpp b/components/misc/pathgridutils.hpp new file mode 100644 index 0000000000..5ca58f4d08 --- /dev/null +++ b/components/misc/pathgridutils.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H +#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H + +#include "convert.hpp" + +#include + +#include + +#include + +namespace Misc +{ + // Slightly cheaper version for comparisons. + // Caller needs to be careful for very short distances (i.e. less than 1) + // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 + // + inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) + { + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); + } + + // Return the closest pathgrid point index from the specified position + // coordinates. NOTE: Does not check if there is a sensible way to get there + // (e.g. a cliff in front). + // + // NOTE: pos is expected to be in local coordinates, as is grid->mPoints + // + inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos) + { + if (grid.mPoints.empty()) + throw std::invalid_argument("Pathgrid has no points"); + + float minDistance = distanceSquared(grid.mPoints[0], pos); + std::size_t closestIndex = 0; + + // TODO: if this full scan causes performance problems mapping pathgrid + // points to a quadtree may help + for (std::size_t i = 1; i < grid.mPoints.size(); ++i) + { + const float distance = distanceSquared(grid.mPoints[i], pos); + if (minDistance > distance) + { + minDistance = distance; + closestIndex = i; + } + } + + return closestIndex; + } +} + +#endif diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 5279e2ad23..c3164b0dfe 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path) return changeExtension(path, ".dds"); } -std::string Misc::ResourceHelpers::correctResourcePath( - std::span topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs) +// If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension. +std::string Misc::ResourceHelpers::correctResourcePath(std::span topLevelDirectories, + std::string_view resPath, const VFS::Manager* vfs, std::string_view ext) { - /* Bethesda at some point converted all their BSA - * textures from tga to dds for increased load speed, but all - * texture file name references were kept as .tga. - */ - std::string correctedPath = Misc::StringUtils::lowerCase(resPath); // Flatten slashes @@ -80,14 +76,14 @@ std::string Misc::ResourceHelpers::correctResourcePath( std::string origExt = correctedPath; - // since we know all (GOTY edition or less) textures end - // in .dds, we change the extension - bool changedToDds = changeExtensionToDds(correctedPath); + // replace extension if `ext` is specified (used for .tga -> .dds, .wav -> .mp3) + bool isExtChanged = !ext.empty() && changeExtension(correctedPath, ext); + if (vfs->exists(correctedPath)) return correctedPath; - // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods) - // verify, and revert if false (this call succeeds quickly, but fails slowly) - if (changedToDds && vfs->exists(origExt)) + + // fall back to original extension + if (isExtChanged && vfs->exists(origExt)) return origExt; // fall back to a resource in the top level directory if it exists @@ -98,7 +94,7 @@ std::string Misc::ResourceHelpers::correctResourcePath( if (vfs->exists(fallback)) return fallback; - if (changedToDds) + if (isExtChanged) { fallback = topLevelDirectories.front(); fallback += '\\'; @@ -110,19 +106,23 @@ std::string Misc::ResourceHelpers::correctResourcePath( return correctedPath; } +// Note: Bethesda at some point converted all their BSA textures from tga to dds for increased load speed, +// but all texture file name references were kept as .tga. So we pass ext=".dds" to all helpers +// looking for textures. + std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs); + return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "icons" } }, resPath, vfs); + return correctResourcePath({ { "icons" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs) { - return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs); + return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs, ".dds"); } std::string Misc::ResourceHelpers::correctBookartPath( @@ -199,6 +199,12 @@ std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath( VFS::Path::NormalizedView resPath, const VFS::Manager& vfs) { + // Note: likely should be replaced with + // return correctResourcePath({ { "sound" } }, resPath, vfs, ".mp3"); + // but there is a slight difference in behaviour: + // - `correctResourcePath(..., ".mp3")` first checks `.mp3`, then tries the original extension + // - the implementation below first tries the original extension, then falls back to `.mp3`. + // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. if (!vfs.exists(resPath)) { diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 9aaa89a861..fb355d6b94 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -1,12 +1,12 @@ #ifndef MISC_RESOURCEHELPERS_H #define MISC_RESOURCEHELPERS_H -#include - #include #include #include +#include + namespace VFS { class Manager; @@ -25,8 +25,8 @@ namespace Misc namespace ResourceHelpers { bool changeExtensionToDds(std::string& path); - std::string correctResourcePath( - std::span topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs); + std::string correctResourcePath(std::span topLevelDirectories, std::string_view resPath, + const VFS::Manager* vfs, std::string_view ext = {}); std::string correctTexturePath(std::string_view resPath, const VFS::Manager* vfs); std::string correctIconPath(std::string_view resPath, const VFS::Manager* vfs); std::string correctBookartPath(std::string_view resPath, const VFS::Manager* vfs); diff --git a/components/misc/utf8qtextstream.hpp b/components/misc/utf8qtextstream.hpp index a1e101864f..1b8bd1d786 100644 --- a/components/misc/utf8qtextstream.hpp +++ b/components/misc/utf8qtextstream.hpp @@ -3,20 +3,13 @@ #include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include -#endif #include namespace Misc { inline void ensureUtf8Encoding(QTextStream& stream) { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - stream.setCodec(QTextCodec::codecForName("UTF-8")); -#else stream.setEncoding(QStringConverter::Utf8); -#endif } } #endif diff --git a/components/myguiplatform/additivelayer.cpp b/components/myguiplatform/additivelayer.cpp index d170c831a6..9ffdc9b84e 100644 --- a/components/myguiplatform/additivelayer.cpp +++ b/components/myguiplatform/additivelayer.cpp @@ -5,7 +5,7 @@ #include "myguirendermanager.hpp" -namespace osgMyGUI +namespace MyGUIPlatform { AdditiveLayer::AdditiveLayer() diff --git a/components/myguiplatform/additivelayer.hpp b/components/myguiplatform/additivelayer.hpp index cfd5c82058..4b5185f97f 100644 --- a/components/myguiplatform/additivelayer.hpp +++ b/components/myguiplatform/additivelayer.hpp @@ -10,7 +10,7 @@ namespace osg class StateSet; } -namespace osgMyGUI +namespace MyGUIPlatform { /// @brief A Layer rendering with additive blend mode. diff --git a/components/myguiplatform/myguidatamanager.cpp b/components/myguiplatform/myguidatamanager.cpp index 49dba3634b..41a2d84e80 100644 --- a/components/myguiplatform/myguidatamanager.cpp +++ b/components/myguiplatform/myguidatamanager.cpp @@ -24,7 +24,7 @@ namespace }; } -namespace osgMyGUI +namespace MyGUIPlatform { void DataManager::setResourcePath(const std::filesystem::path& path) diff --git a/components/myguiplatform/myguidatamanager.hpp b/components/myguiplatform/myguidatamanager.hpp index 5b392177b7..f7489f8b65 100644 --- a/components/myguiplatform/myguidatamanager.hpp +++ b/components/myguiplatform/myguidatamanager.hpp @@ -11,7 +11,7 @@ namespace VFS class Manager; } -namespace osgMyGUI +namespace MyGUIPlatform { class DataManager : public MyGUI::DataManager diff --git a/components/myguiplatform/myguiloglistener.cpp b/components/myguiplatform/myguiloglistener.cpp index 3e52e75ad2..66b35e0961 100644 --- a/components/myguiplatform/myguiloglistener.cpp +++ b/components/myguiplatform/myguiloglistener.cpp @@ -4,7 +4,7 @@ #include -namespace osgMyGUI +namespace MyGUIPlatform { void CustomLogListener::open() { diff --git a/components/myguiplatform/myguiloglistener.hpp b/components/myguiplatform/myguiloglistener.hpp index 15cea0effd..3557f56540 100644 --- a/components/myguiplatform/myguiloglistener.hpp +++ b/components/myguiplatform/myguiloglistener.hpp @@ -10,7 +10,7 @@ #include #include -namespace osgMyGUI +namespace MyGUIPlatform { /// \brief Custom MyGUI::ILogListener interface implementation diff --git a/components/myguiplatform/myguiplatform.cpp b/components/myguiplatform/myguiplatform.cpp index 20fdaa7e7c..9d24e13ca1 100644 --- a/components/myguiplatform/myguiplatform.cpp +++ b/components/myguiplatform/myguiplatform.cpp @@ -6,7 +6,7 @@ #include "components/files/conversion.hpp" -namespace osgMyGUI +namespace MyGUIPlatform { Platform::Platform(osgViewer::Viewer* viewer, osg::Group* guiRoot, Resource::ImageManager* imageManager, diff --git a/components/myguiplatform/myguiplatform.hpp b/components/myguiplatform/myguiplatform.hpp index 66b02cd8ba..ff7e4a339f 100644 --- a/components/myguiplatform/myguiplatform.hpp +++ b/components/myguiplatform/myguiplatform.hpp @@ -26,7 +26,7 @@ namespace VFS class Manager; } -namespace osgMyGUI +namespace MyGUIPlatform { class RenderManager; diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 5d32641b6d..a17e387ed6 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -40,12 +40,12 @@ } \ } while (0) -namespace osgMyGUI +namespace MyGUIPlatform { class Drawable : public osg::Drawable { - osgMyGUI::RenderManager* mParent; + MyGUIPlatform::RenderManager* mParent; osg::ref_ptr mStateSet; public: @@ -58,12 +58,12 @@ namespace osgMyGUI { } - void setRenderManager(osgMyGUI::RenderManager* renderManager) { mRenderManager = renderManager; } + void setRenderManager(MyGUIPlatform::RenderManager* renderManager) { mRenderManager = renderManager; } void operator()(osg::Node*, osg::NodeVisitor*) { mRenderManager->update(); } private: - osgMyGUI::RenderManager* mRenderManager; + MyGUIPlatform::RenderManager* mRenderManager; }; // Stage 1: collect draw calls. Run during the Cull traversal. @@ -75,12 +75,12 @@ namespace osgMyGUI { } - void setRenderManager(osgMyGUI::RenderManager* renderManager) { mRenderManager = renderManager; } + void setRenderManager(MyGUIPlatform::RenderManager* renderManager) { mRenderManager = renderManager; } void operator()(osg::Node*, osg::NodeVisitor*) { mRenderManager->collectDrawCalls(); } private: - osgMyGUI::RenderManager* mRenderManager; + MyGUIPlatform::RenderManager* mRenderManager; }; // Stage 2: execute the draw calls. Run during the Draw traversal. May run in parallel with the update traversal @@ -162,7 +162,7 @@ namespace osgMyGUI } public: - Drawable(osgMyGUI::RenderManager* parent = nullptr) + Drawable(MyGUIPlatform::RenderManager* parent = nullptr) : mParent(parent) , mWriteTo(0) , mReadFrom(0) diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp index 7f1582203a..737a0eb21e 100644 --- a/components/myguiplatform/myguirendermanager.hpp +++ b/components/myguiplatform/myguirendermanager.hpp @@ -28,7 +28,7 @@ namespace osg class StateSet; } -namespace osgMyGUI +namespace MyGUIPlatform { class Drawable; diff --git a/components/myguiplatform/myguitexture.cpp b/components/myguiplatform/myguitexture.cpp index 9d865e1296..529488beb1 100644 --- a/components/myguiplatform/myguitexture.cpp +++ b/components/myguiplatform/myguitexture.cpp @@ -8,7 +8,7 @@ #include #include -namespace osgMyGUI +namespace MyGUIPlatform { OSGTexture::OSGTexture(const std::string& name, Resource::ImageManager* imageManager) diff --git a/components/myguiplatform/myguitexture.hpp b/components/myguiplatform/myguitexture.hpp index 7139a81dba..3e34820069 100644 --- a/components/myguiplatform/myguitexture.hpp +++ b/components/myguiplatform/myguitexture.hpp @@ -17,7 +17,7 @@ namespace Resource class ImageManager; } -namespace osgMyGUI +namespace MyGUIPlatform { class OSGTexture final : public MyGUI::ITexture diff --git a/components/myguiplatform/scalinglayer.cpp b/components/myguiplatform/scalinglayer.cpp index c04134bfad..1660e4f0ca 100644 --- a/components/myguiplatform/scalinglayer.cpp +++ b/components/myguiplatform/scalinglayer.cpp @@ -3,7 +3,7 @@ #include #include -namespace osgMyGUI +namespace MyGUIPlatform { /// @brief the ProxyRenderTarget allows to adjust the pixel scale and offset for a "source" render target. diff --git a/components/myguiplatform/scalinglayer.hpp b/components/myguiplatform/scalinglayer.hpp index 4f04ce917a..512bb0109e 100644 --- a/components/myguiplatform/scalinglayer.hpp +++ b/components/myguiplatform/scalinglayer.hpp @@ -3,7 +3,7 @@ #include -namespace osgMyGUI +namespace MyGUIPlatform { ///@brief A Layer that lays out and renders widgets in screen-relative coordinates. The "Size" property determines diff --git a/components/nif/data.cpp b/components/nif/data.cpp index 29b11bd806..a134749bdc 100644 --- a/components/nif/data.cpp +++ b/components/nif/data.cpp @@ -247,16 +247,11 @@ namespace Nif void NiVisData::read(NIFStream* nif) { - mKeys = std::make_shared>(); - uint32_t numKeys; - nif->read(numKeys); - for (size_t i = 0; i < numKeys; i++) + mKeys = std::make_shared>>(nif->get()); + for (auto& [time, value] : *mKeys) { - float time; - char value; nif->read(time); - nif->read(value); - (*mKeys)[time] = (value != 0); + value = nif->get() != 0; } } diff --git a/components/nif/data.hpp b/components/nif/data.hpp index 1ccd2919b7..4000055e8c 100644 --- a/components/nif/data.hpp +++ b/components/nif/data.hpp @@ -193,8 +193,8 @@ namespace Nif struct NiVisData : public Record { - // TODO: investigate possible use of BoolKeyMap - std::shared_ptr> mKeys; + // This is theoretically a "flat map" sorted by time + std::shared_ptr>> mKeys; void read(NIFStream* nif) override; }; diff --git a/components/nif/nifkey.hpp b/components/nif/nifkey.hpp index bd362101c6..e32ef76d95 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -3,8 +3,7 @@ #ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP #define OPENMW_COMPONENTS_NIF_NIFKEY_HPP -#include -#include +#include #include #include "exception.hpp" @@ -47,7 +46,8 @@ namespace Nif template struct KeyMapT { - using MapType = std::map>; + // This is theoretically a "flat map" sorted by time + using MapType = std::vector>>; using ValueType = T; using KeyType = KeyT; @@ -79,8 +79,12 @@ namespace Nif uint32_t count; nif->read(count); - if (count != 0 || morph) - nif->read(mInterpolationType); + if (count == 0 && !morph) + return; + + nif->read(mInterpolationType); + + mKeys.reserve(count); KeyType key = {}; @@ -91,7 +95,7 @@ namespace Nif float time; nif->read(time); readValue(*nif, key); - mKeys[time] = key; + mKeys.emplace_back(time, key); } } else if (mInterpolationType == InterpolationType_Quadratic) @@ -101,23 +105,24 @@ namespace Nif float time; nif->read(time); readQuadratic(*nif, key); - mKeys[time] = key; + mKeys.emplace_back(time, key); } } else if (mInterpolationType == InterpolationType_TCB) { std::vector> tcbKeys(count); - for (TCBKey& key : tcbKeys) + for (TCBKey& tcbKey : tcbKeys) { - nif->read(key.mTime); - key.mValue = ((*nif).*getValue)(); - nif->read(key.mTension); - nif->read(key.mContinuity); - nif->read(key.mBias); + nif->read(tcbKey.mTime); + tcbKey.mValue = ((*nif).*getValue)(); + nif->read(tcbKey.mTension); + nif->read(tcbKey.mContinuity); + nif->read(tcbKey.mBias); } generateTCBTangents(tcbKeys); - for (TCBKey& key : tcbKeys) - mKeys[key.mTime] = KeyType{ std::move(key.mValue), std::move(key.mInTan), std::move(key.mOutTan) }; + for (TCBKey& tcbKey : tcbKeys) + mKeys.emplace_back(std::move(tcbKey.mTime), + KeyType{ std::move(tcbKey.mValue), std::move(tcbKey.mInTan), std::move(tcbKey.mOutTan) }); } else if (mInterpolationType == InterpolationType_XYZ) { @@ -133,6 +138,8 @@ namespace Nif throw Nif::Exception("Unhandled interpolation type: " + std::to_string(mInterpolationType), nif->getFile().getFilename()); } + + // Note: NetImmerse does NOT sort keys or remove duplicates } private: @@ -154,35 +161,23 @@ namespace Nif if (keys.size() <= 1) return; - std::sort(keys.begin(), keys.end(), [](const auto& a, const auto& b) { return a.mTime < b.mTime; }); - for (size_t i = 0; i < keys.size(); ++i) + for (std::size_t i = 0; i < keys.size(); ++i) { TCBKey& curr = keys[i]; - const TCBKey& prev = (i == 0) ? curr : keys[i - 1]; - const TCBKey& next = (i == keys.size() - 1) ? curr : keys[i + 1]; - const float prevLen = curr.mTime - prev.mTime; - const float nextLen = next.mTime - curr.mTime; - if (prevLen + nextLen <= 0.f) + const TCBKey* prev = (i == 0) ? nullptr : &keys[i - 1]; + const TCBKey* next = (i == keys.size() - 1) ? nullptr : &keys[i + 1]; + const float prevLen = prev != nullptr && next != nullptr ? curr.mTime - prev->mTime : 1.f; + const float nextLen = prev != nullptr && next != nullptr ? next->mTime - curr.mTime : 1.f; + if (prevLen + nextLen == 0.f) continue; - - const U prevDelta = curr.mValue - prev.mValue; - const U nextDelta = next.mValue - curr.mValue; - const float t = curr.mTension; - const float c = curr.mContinuity; - const float b = curr.mBias; - - U x{}, y{}, z{}, w{}; - if (prevLen > 0.f) - x = prevDelta / prevLen * (1 - t) * (1 - c) * (1 + b); - if (nextLen > 0.f) - y = nextDelta / nextLen * (1 - t) * (1 + c) * (1 - b); - if (prevLen > 0.f) - z = prevDelta / prevLen * (1 - t) * (1 + c) * (1 + b); - if (nextLen > 0.f) - w = nextDelta / nextLen * (1 - t) * (1 - c) * (1 - b); - - curr.mInTan = (x + y) * prevLen / (prevLen + nextLen); - curr.mOutTan = (z + w) * nextLen / (prevLen + nextLen); + const float x = (1.f - curr.mTension) * (1.f - curr.mContinuity) * (1.f + curr.mBias); + const float y = (1.f - curr.mTension) * (1.f + curr.mContinuity) * (1.f - curr.mBias); + const float z = (1.f - curr.mTension) * (1.f + curr.mContinuity) * (1.f + curr.mBias); + const float w = (1.f - curr.mTension) * (1.f - curr.mContinuity) * (1.f - curr.mBias); + const U prevDelta = prev != nullptr ? curr.mValue - prev->mValue : next->mValue - curr.mValue; + const U nextDelta = next != nullptr ? next->mValue - curr.mValue : curr.mValue - prev->mValue; + curr.mInTan = (prevDelta * x + nextDelta * y) * prevLen / (prevLen + nextLen); + curr.mOutTan = (prevDelta * z + nextDelta * w) * nextLen / (prevLen + nextLen); } } @@ -196,6 +191,7 @@ namespace Nif // TODO: implement TCB interpolation for quaternions } }; + using FloatKeyMap = KeyMapT>; using Vector3KeyMap = KeyMapT>; using Vector4KeyMap = KeyMapT>; diff --git a/components/nif/particle.cpp b/components/nif/particle.cpp index d81d423fb6..9249541717 100644 --- a/components/nif/particle.cpp +++ b/components/nif/particle.cpp @@ -676,12 +676,16 @@ namespace Nif void NiPSysEmitterCtlrData::read(NIFStream* nif) { + // TODO: this is not used in the official files and needs verification mFloatKeyList = std::make_shared(); + mFloatKeyList->read(nif); mVisKeyList = std::make_shared(); - uint32_t numVisKeys; - nif->read(numVisKeys); - for (size_t i = 0; i < numVisKeys; i++) - mVisKeyList->mKeys[nif->get()].mValue = nif->get() != 0; + mVisKeyList->mKeys.resize(nif->get()); + for (auto& [time, key] : mVisKeyList->mKeys) + { + nif->read(time); + key.mValue = nif->get() != 0; + } } void NiPSysCollider::read(NIFStream* nif) diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 7e4c5da7a0..ee3d0dd45e 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -374,7 +374,8 @@ namespace NifOsg if (mData->empty()) return true; - auto iter = mData->upper_bound(time); + auto iter = std::upper_bound(mData->begin(), mData->end(), time, + [](float time, const std::pair& key) { return time < key.first; }); if (iter != mData->begin()) --iter; return iter->second; diff --git a/components/nifosg/controller.hpp b/components/nifosg/controller.hpp index 468668ce76..cceec279b2 100644 --- a/components/nifosg/controller.hpp +++ b/components/nifosg/controller.hpp @@ -54,7 +54,8 @@ namespace NifOsg return mLastHighKey; } - return mKeys->mKeys.lower_bound(time); + return std::lower_bound(mKeys->mKeys.begin(), mKeys->mKeys.end(), time, + [](const typename MapT::MapType::value_type& key, float t) { return key.first < t; }); } public: @@ -99,8 +100,8 @@ namespace NifOsg const typename MapT::MapType& keys = mKeys->mKeys; - if (time <= keys.begin()->first) - return keys.begin()->second.mValue; + if (time <= keys.front().first) + return keys.front().second.mValue; typename MapT::MapType::const_iterator it = retrieveKey(time); @@ -111,12 +112,17 @@ namespace NifOsg mLastHighKey = it; mLastLowKey = --it; - float a = (time - mLastLowKey->first) / (mLastHighKey->first - mLastLowKey->first); + const float highTime = mLastHighKey->first; + const float lowTime = mLastLowKey->first; + if (highTime == lowTime) + return mLastLowKey->second.mValue; + + const float a = (time - lowTime) / (highTime - lowTime); return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType); } - return keys.rbegin()->second.mValue; + return keys.back().second.mValue; } bool empty() const { return !mKeys || mKeys->mKeys.empty(); } @@ -283,7 +289,7 @@ namespace NifOsg class VisController : public SceneUtil::NodeCallback, public SceneUtil::Controller { private: - std::shared_ptr> mData; + std::shared_ptr>> mData; BoolInterpolator mInterpolator; unsigned int mMask{ 0u }; diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 07eb342221..de5aa01b15 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -1568,6 +1568,9 @@ namespace NifOsg } rig->setBoneInfo(std::move(boneInfo)); rig->setInfluences(influences); + rig->setTransform(data->mTransform.toMatrix()); + if (const Nif::NiAVObject* rootBone = skin->mRoot.getPtr()) + rig->setRootBone(rootBone->mName); drawable = rig; } @@ -1683,7 +1686,7 @@ namespace NifOsg if (hasColors) colors.emplace_back(elem.mVertColor[0], elem.mVertColor[1], elem.mVertColor[2], elem.mVertColor[3]); if (hasUV) - uvlist.emplace_back(halfToFloat(elem.mUV[0]), 1.0 - halfToFloat(elem.mUV[1])); + uvlist.emplace_back(halfToFloat(elem.mUV[0]), 1.0f - halfToFloat(elem.mUV[1])); } if (!vertices.empty()) @@ -1729,6 +1732,8 @@ namespace NifOsg } rig->setBoneInfo(std::move(boneInfo)); rig->setInfluences(influences); + if (const Nif::NiAVObject* rootBone = skin->mRoot.getPtr()) + rig->setRootBone(rootBone->mName); drawable = rig; } diff --git a/components/resource/objectcache.hpp b/components/resource/objectcache.hpp index e619b7102c..2ff25c92f1 100644 --- a/components/resource/objectcache.hpp +++ b/components/resource/objectcache.hpp @@ -53,28 +53,49 @@ namespace Resource class GenericObjectCache : public osg::Referenced { public: - // Update last usage timestamp using referenceTime for each cache time if they are not nullptr and referenced - // from somewhere else. Remove items with last usage > expiryTime. Note: last usage might be updated from other - // places so nullptr or not references elsewhere items are not always removed. + /* + * @brief Updates usage timestamps and removes expired items + * + * Updates the lastUsage timestamp of cached non-nullptr items that have external references. + * Initializes lastUsage timestamp for new items. + * Removes items that haven't been referenced for longer than expiryDelay. + * + * \note + * Last usage might be updated from other places so nullptr items + * that are not referenced elsewhere are not always removed. + * + * @param referenceTime the timestamp indicating when the item was most recently used + * @param expiryDelay the delay after which the cache entry for an item expires + */ void update(double referenceTime, double expiryDelay) { std::vector> objectsToRemove; { const double expiryTime = referenceTime - expiryDelay; std::lock_guard lock(mMutex); + std::erase_if(mItems, [&](auto& v) { Item& item = v.second; + + // update last usage timestamp if item is being referenced externally + // or initialize if not set if ((item.mValue != nullptr && item.mValue->referenceCount() > 1) || item.mLastUsage == 0) item.mLastUsage = referenceTime; + + // skip items that have been accessed since expiryTime if (item.mLastUsage > expiryTime) return false; + ++mExpired; + + // just mark for removal here so objects can be removed in bulk outside the lock if (item.mValue != nullptr) objectsToRemove.push_back(std::move(item.mValue)); + return true; }); } - // note, actual unref happens outside of the lock + // remove expired items from cache objectsToRemove.clear(); } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index cbda399120..6100d0ccaa 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -607,6 +608,9 @@ namespace Resource if (!getSupportsNormalsRT()) return; stateset->setAttributeAndModes(new osg::ColorMaski(1, enabled, enabled, enabled, enabled)); + + if (enabled) + stateset->setAttributeAndModes(new osg::Disablei(GL_BLEND, 1)); } /// @brief Callback to read image files from the VFS. @@ -971,6 +975,14 @@ namespace Resource return loadNonNif(errorMarker, file, mImageManager); } + void SceneManager::loadSelectionMarker( + osg::ref_ptr parentNode, const char* markerData, long long markerSize) const + { + Files::IMemStream file(markerData, markerSize); + constexpr VFS::Path::NormalizedView selectionMarker("selectionmarker.osgt"); + parentNode->addChild(loadNonNif(selectionMarker, file, mImageManager)); + } + osg::ref_ptr SceneManager::cloneErrorMarker() { if (!mErrorMarker) diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index 219ecd2a94..8141b5308f 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -136,6 +136,9 @@ namespace Resource osg::ref_ptr getOpaqueDepthTex(size_t frame); + void loadSelectionMarker( + osg::ref_ptr parentNode, const char* markerData, long long markerSize) const; + enum class UBOBinding { // If we add more UBO's, we should probably assign their bindings dynamically according to the current count diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index d93b88349d..475522660e 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "skeleton.hpp" @@ -181,6 +182,12 @@ namespace SceneUtil ++boneInfo; } + osg::Matrixf transform; + if (mSkinToSkelMatrix) + transform = (*mSkinToSkelMatrix) * mData->mTransform; + else + transform = mData->mTransform; + for (const auto& [influences, vertices] : mData->mInfluences) { osg::Matrixf resultMat(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1); @@ -196,8 +203,7 @@ namespace SceneUtil *resultMatPtr += *boneMatPtr * weight; } - if (mGeomToSkelMatrix) - resultMat *= (*mGeomToSkelMatrix); + resultMat *= transform; for (unsigned short vertex : vertices) { @@ -242,9 +248,14 @@ namespace SceneUtil mSkeleton->updateBoneMatrices(nv->getTraversalNumber()); - updateGeomToSkelMatrix(nv->getNodePath()); + updateSkinToSkelMatrix(nv->getNodePath()); osg::BoundingBox box; + osg::Matrixf transform; + if (mSkinToSkelMatrix) + transform = (*mSkinToSkelMatrix) * mData->mTransform; + else + transform = mData->mTransform; size_t index = 0; for (const BoneInfo& info : mData->mBones) @@ -254,10 +265,7 @@ namespace SceneUtil continue; osg::BoundingSpheref bs = info.mBoundSphere; - if (mGeomToSkelMatrix) - transformBoundingSphere(bone->mMatrixInSkeletonSpace * (*mGeomToSkelMatrix), bs); - else - transformBoundingSphere(bone->mMatrixInSkeletonSpace, bs); + transformBoundingSphere(bone->mMatrixInSkeletonSpace * transform, bs); box.expandBy(bs); } @@ -280,31 +288,39 @@ namespace SceneUtil } } - void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath) + void RigGeometry::updateSkinToSkelMatrix(const osg::NodePath& nodePath) { - bool foundSkel = false; - osg::RefMatrix* geomToSkelMatrix = mGeomToSkelMatrix; - if (geomToSkelMatrix) - geomToSkelMatrix->makeIdentity(); - for (osg::NodePath::const_iterator it = nodePath.begin(); it != nodePath.end() - 1; ++it) + if (mSkinToSkelMatrix) + mSkinToSkelMatrix->makeIdentity(); + auto skeletonRoot = std::find(nodePath.begin(), nodePath.end(), mSkeleton); + if (skeletonRoot == nodePath.end()) + return; + skeletonRoot++; + auto skinRoot = nodePath.end(); + if (!mData->mRootBone.empty()) + skinRoot = std::find_if(skeletonRoot, nodePath.end(), + [&](const osg::Node* node) { return Misc::StringUtils::ciEqual(node->getName(), mData->mRootBone); }); + if (skinRoot == nodePath.end()) { - osg::Node* node = *it; - if (!foundSkel) + // Failed to find skin root, cancel out everything up till the trishape. + // Our parent node is the trishape's transform + skinRoot = nodePath.end() - 2; + if ((*skinRoot)->getName() != getName()) // but maybe it can get optimized out + skinRoot++; + } + else + skinRoot++; + for (auto it = skeletonRoot; it != skinRoot; ++it) + { + const osg::Node* node = *it; + if (const osg::Transform* trans = node->asTransform()) { - if (node == mSkeleton) - foundSkel = true; - } - else - { - if (osg::Transform* trans = node->asTransform()) - { - osg::MatrixTransform* matrixTrans = trans->asMatrixTransform(); - if (matrixTrans && matrixTrans->getMatrix().isIdentity()) - continue; - if (!geomToSkelMatrix) - geomToSkelMatrix = mGeomToSkelMatrix = new osg::RefMatrix; - trans->computeWorldToLocalMatrix(*geomToSkelMatrix, nullptr); - } + const osg::MatrixTransform* matrixTrans = trans->asMatrixTransform(); + if (matrixTrans && matrixTrans->getMatrix().isIdentity()) + continue; + if (!mSkinToSkelMatrix) + mSkinToSkelMatrix = new osg::RefMatrix; + trans->computeWorldToLocalMatrix(*mSkinToSkelMatrix, nullptr); } } } @@ -346,12 +362,26 @@ namespace SceneUtil std::map influencesToVertices; for (size_t i = 0; i < influences.size(); i++) - influencesToVertices[influences[i]].emplace_back(i); + influencesToVertices[influences[i]].emplace_back(static_cast(i)); mData->mInfluences.reserve(influencesToVertices.size()); mData->mInfluences.assign(influencesToVertices.begin(), influencesToVertices.end()); } + void RigGeometry::setTransform(osg::Matrixf&& transform) + { + if (!mData) + mData = new InfluenceData; + mData->mTransform = transform; + } + + void RigGeometry::setRootBone(std::string_view name) + { + if (!mData) + mData = new InfluenceData; + mData->mRootBone = name; + } + void RigGeometry::accept(osg::NodeVisitor& nv) { if (!nv.validNodeMask(*this)) diff --git a/components/sceneutil/riggeometry.hpp b/components/sceneutil/riggeometry.hpp index 64ea1e2519..670c040758 100644 --- a/components/sceneutil/riggeometry.hpp +++ b/components/sceneutil/riggeometry.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace SceneUtil { class Skeleton; @@ -58,6 +60,10 @@ namespace SceneUtil /// @note The source geometry will not be modified. void setSourceGeometry(osg::ref_ptr sourceGeom); + void setTransform(osg::Matrixf&& transform); + + void setRootBone(std::string_view name); + osg::ref_ptr getSourceGeometry() const; void accept(osg::NodeVisitor& nv) override; @@ -89,13 +95,15 @@ namespace SceneUtil osg::ref_ptr mSourceTangents; Skeleton* mSkeleton{ nullptr }; - osg::ref_ptr mGeomToSkelMatrix; + osg::ref_ptr mSkinToSkelMatrix; using VertexList = std::vector; struct InfluenceData : public osg::Referenced { std::vector mBones; std::vector> mInfluences; + osg::Matrixf mTransform; + std::string mRootBone; }; osg::ref_ptr mData; std::vector mNodes; @@ -105,7 +113,7 @@ namespace SceneUtil bool initFromParentSkeleton(osg::NodeVisitor* nv); - void updateGeomToSkelMatrix(const osg::NodePath& nodePath); + void updateSkinToSkelMatrix(const osg::NodePath& nodePath); }; } diff --git a/components/sdlutil/events.hpp b/components/sdlutil/events.hpp index 3ae15e448c..410ee68440 100644 --- a/components/sdlutil/events.hpp +++ b/components/sdlutil/events.hpp @@ -28,7 +28,6 @@ namespace SDLUtil float mY; float mPressure; -#if SDL_VERSION_ATLEAST(2, 0, 14) explicit TouchEvent(const SDL_ControllerTouchpadEvent& arg) : mDevice(arg.touchpad) , mFinger(arg.finger) @@ -37,7 +36,6 @@ namespace SDLUtil , mPressure(arg.pressure) { } -#endif }; /////////////// diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 41a4f13818..104f83fc6d 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -173,7 +173,6 @@ namespace SDLUtil if (mConListener) mConListener->axisMoved(1, evt.caxis); break; -#if SDL_VERSION_ATLEAST(2, 0, 14) case SDL_CONTROLLERSENSORUPDATE: // controller sensor data is received on demand break; @@ -186,7 +185,6 @@ namespace SDLUtil case SDL_CONTROLLERTOUCHPADUP: mConListener->touchpadReleased(1, TouchEvent(evt.ctouchpad)); break; -#endif case SDL_WINDOWEVENT: handleWindowEvent(evt); break; diff --git a/components/settings/categories/game.hpp b/components/settings/categories/game.hpp index ec6f9dc206..ac5a9a08c3 100644 --- a/components/settings/categories/game.hpp +++ b/components/settings/categories/game.hpp @@ -71,8 +71,6 @@ namespace Settings SettingValue mDefaultActorPathfindHalfExtents{ mIndex, "Game", "default actor pathfind half extents", makeMaxStrictSanitizerVec3f(osg::Vec3f(0, 0, 0)) }; SettingValue mDayNightSwitches{ mIndex, "Game", "day night switches" }; - SettingValue mUnarmedCreatureAttacksDamageArmor{ mIndex, "Game", - "unarmed creature attacks damage armor" }; SettingValue mActorCollisionShapeType{ mIndex, "Game", "actor collision shape type" }; SettingValue mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" }; diff --git a/components/settings/categories/gui.hpp b/components/settings/categories/gui.hpp index a26364c5dd..ffef047094 100644 --- a/components/settings/categories/gui.hpp +++ b/components/settings/categories/gui.hpp @@ -10,8 +10,6 @@ #include -#include -#include #include namespace Settings diff --git a/components/terrain/chunkmanager.cpp b/components/terrain/chunkmanager.cpp index 7ccd89ac21..3d7c2b1dfc 100644 --- a/components/terrain/chunkmanager.cpp +++ b/components/terrain/chunkmanager.cpp @@ -19,6 +19,23 @@ namespace Terrain { + struct UpdateTextureFilteringFunctor + { + UpdateTextureFilteringFunctor(Resource::SceneManager* sceneMgr) + : mSceneManager(sceneMgr) + { + } + Resource::SceneManager* mSceneManager; + + void operator()(ChunkKey, osg::Object* obj) + { + TerrainDrawable* drawable = static_cast(obj); + CompositeMap* composite = drawable->getCompositeMap(); + if (composite && composite->mTexture) + mSceneManager->applyFilterSettings(composite->mTexture); + } + }; + ChunkManager::ChunkManager(Storage* storage, Resource::SceneManager* sceneMgr, TextureManager* textureManager, CompositeMapRenderer* renderer, ESM::RefId worldspace, double expiryDelay) : GenericResourceManager(nullptr, expiryDelay) @@ -61,6 +78,12 @@ namespace Terrain return node; } + void ChunkManager::updateTextureFiltering() + { + UpdateTextureFilteringFunctor f(mSceneManager); + mCache->call(f); + } + void ChunkManager::reportStats(unsigned int frameNumber, osg::Stats* stats) const { Resource::reportStats("Terrain Chunk", frameNumber, mCache->getStats(), *stats); @@ -85,10 +108,9 @@ namespace Terrain texture->setTextureWidth(mCompositeMapSize); texture->setTextureHeight(mCompositeMapSize); texture->setInternalFormat(GL_RGB); - texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); texture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mSceneManager->applyFilterSettings(texture); return texture; } diff --git a/components/terrain/chunkmanager.hpp b/components/terrain/chunkmanager.hpp index 20d6ba9327..b4f327e699 100644 --- a/components/terrain/chunkmanager.hpp +++ b/components/terrain/chunkmanager.hpp @@ -85,6 +85,8 @@ namespace Terrain void setCompositeMapLevel(float level) { mCompositeMapLevel = level; } void setMaxCompositeGeometrySize(float maxCompGeometrySize) { mMaxCompGeometrySize = maxCompGeometrySize; } + void updateTextureFiltering(); + void setNodeMask(unsigned int mask) { mNodeMask = mask; } unsigned int getNodeMask() override { return mNodeMask; } diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 09d2680acd..9c3a7f589d 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -43,7 +43,8 @@ namespace // blendmap, apparently. matrix.preMultTranslate(osg::Vec3f(1.0f / blendmapScale / 4.0f, 1.0f / blendmapScale / 4.0f, 0.f)); - texMat = mTexMatMap.insert(std::make_pair(blendmapScale, new osg::TexMat(matrix))).first; + texMat = mTexMatMap.insert(std::make_pair(static_cast(blendmapScale), new osg::TexMat(matrix))) + .first; } return texMat->second; } diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 58cfc0b068..e18881e490 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -133,6 +133,8 @@ namespace Terrain { if (mTextureManager) mTextureManager->updateTextureFiltering(); + if (mChunkManager) + mChunkManager->updateTextureFiltering(); } void World::clearAssociatedCaches() diff --git a/components/testing/util.hpp b/components/testing/util.hpp index 53331d6d37..e2183d5403 100644 --- a/components/testing/util.hpp +++ b/components/testing/util.hpp @@ -56,7 +56,9 @@ namespace TestingOpenMW Files::IStreamPtr open() override { return std::make_unique(mContent, std::ios_base::in); } - std::filesystem::path getPath() override { return "TestFile"; } + std::filesystem::file_time_type getLastModified() const override { return {}; } + + std::string getStem() const override { return "TestFile"; } private: const std::string mContent; diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index 2e6fac6558..f89e47d971 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -10,45 +10,72 @@ #include #include +#include + #include #include #include namespace VFS { + template + class BsaArchive; + template class BsaArchiveFile : public File { public: - BsaArchiveFile(const Bsa::BSAFile::FileStruct* info, FileType* bsa) + BsaArchiveFile(const Bsa::BSAFile::FileStruct* info, const BsaArchive* bsa) : mInfo(info) , mFile(bsa) { } - Files::IStreamPtr open() override { return mFile->getFile(mInfo); } + Files::IStreamPtr open() override { return mFile->getFile()->getFile(mInfo); } - std::filesystem::path getPath() override { return mInfo->name(); } + std::filesystem::file_time_type getLastModified() const override + { + return std::filesystem::last_write_time(mFile->getFile()->getPath()); + } + + std::string getStem() const override + { + std::string_view name = mInfo->name(); + auto index = name.find_last_of("\\/"); + if (index != std::string_view::npos) + name = name.substr(index + 1); + index = name.find_last_of('.'); + if (index != std::string_view::npos && index != 0) + name = name.substr(0, index); + std::string out; + std::string_view utf8 = mFile->getUtf8(name, out); + if (out.data() == utf8.data()) + out.resize(utf8.size()); + else + out = utf8; + return out; + } const Bsa::BSAFile::FileStruct* mInfo; - FileType* mFile; + const BsaArchive* mFile; }; template class BsaArchive : public Archive { public: - BsaArchive(const std::filesystem::path& filename) + BsaArchive(const std::filesystem::path& filename, const ToUTF8::StatelessUtf8Encoder* encoder) : Archive() + , mEncoder(encoder) { mFile = std::make_unique(); mFile->open(filename); - const Bsa::BSAFile::FileList& filelist = mFile->getList(); - for (Bsa::BSAFile::FileList::const_iterator it = filelist.begin(); it != filelist.end(); ++it) + std::string buffer; + for (const Bsa::BSAFile::FileStruct& file : mFile->getList()) { - mResources.emplace_back(&*it, mFile.get()); - mFiles.emplace_back(it->name()); + mResources.emplace_back(&file, this); + mFiles.emplace_back(getUtf8(file.name(), buffer)); } std::sort(mFiles.begin(), mFiles.end()); @@ -56,8 +83,12 @@ namespace VFS void listResources(FileMap& out) override { + std::string buffer; for (auto& resource : mResources) - out[VFS::Path::Normalized(resource.mInfo->name())] = &resource; + { + std::string_view path = getUtf8(resource.mInfo->name(), buffer); + out[VFS::Path::Normalized(path)] = &resource; + } } bool contains(Path::NormalizedView file) const override @@ -67,26 +98,37 @@ namespace VFS std::string getDescription() const override { return std::string{ "BSA: " } + mFile->getFilename(); } + BSAFileType* getFile() const { return mFile.get(); } + + std::string_view getUtf8(std::string_view input, std::string& buffer) const + { + if (mEncoder == nullptr) + return input; + return mEncoder->getUtf8(input, ToUTF8::BufferAllocationPolicy::UseGrowFactor, buffer); + } + private: std::unique_ptr mFile; std::vector> mResources; std::vector mFiles; + const ToUTF8::StatelessUtf8Encoder* mEncoder; }; - inline std::unique_ptr makeBsaArchive(const std::filesystem::path& path) + inline std::unique_ptr makeBsaArchive( + const std::filesystem::path& path, const ToUTF8::StatelessUtf8Encoder* encoder) { switch (Bsa::BSAFile::detectVersion(path)) { case Bsa::BsaVersion::Unknown: break; case Bsa::BsaVersion::Uncompressed: - return std::make_unique>(path); + return std::make_unique>(path, encoder); case Bsa::BsaVersion::Compressed: - return std::make_unique>(path); + return std::make_unique>(path, encoder); case Bsa::BsaVersion::BA2GNRL: - return std::make_unique>(path); + return std::make_unique>(path, encoder); case Bsa::BsaVersion::BA2DX10: - return std::make_unique>(path); + return std::make_unique>(path, encoder); } throw std::runtime_error("Unknown archive type '" + Files::pathToUnicodeString(path) + "'"); diff --git a/components/vfs/file.hpp b/components/vfs/file.hpp index f2dadb1162..7c65e3a1ba 100644 --- a/components/vfs/file.hpp +++ b/components/vfs/file.hpp @@ -2,6 +2,7 @@ #define OPENMW_COMPONENTS_VFS_FILE_H #include +#include #include @@ -14,7 +15,9 @@ namespace VFS virtual Files::IStreamPtr open() = 0; - virtual std::filesystem::path getPath() = 0; + virtual std::filesystem::file_time_type getLastModified() const = 0; + + virtual std::string getStem() const = 0; }; } diff --git a/components/vfs/filesystemarchive.cpp b/components/vfs/filesystemarchive.cpp index 3303c6656c..0b67df45bc 100644 --- a/components/vfs/filesystemarchive.cpp +++ b/components/vfs/filesystemarchive.cpp @@ -81,4 +81,14 @@ namespace VFS return Files::openConstrainedFileStream(mPath); } + std::filesystem::file_time_type FileSystemArchiveFile::getLastModified() const + { + return std::filesystem::last_write_time(mPath); + } + + std::string FileSystemArchiveFile::getStem() const + { + return Files::pathToUnicodeString(mPath.stem()); + } + } diff --git a/components/vfs/filesystemarchive.hpp b/components/vfs/filesystemarchive.hpp index 215c443b58..f6b1a2ec5e 100644 --- a/components/vfs/filesystemarchive.hpp +++ b/components/vfs/filesystemarchive.hpp @@ -17,7 +17,9 @@ namespace VFS Files::IStreamPtr open() override; - std::filesystem::path getPath() override { return mPath; } + std::filesystem::file_time_type getLastModified() const override; + + std::string getStem() const override; private: std::filesystem::path mPath; diff --git a/components/vfs/manager.cpp b/components/vfs/manager.cpp index 12ef378017..ff25150f67 100644 --- a/components/vfs/manager.cpp +++ b/components/vfs/manager.cpp @@ -81,15 +81,20 @@ namespace VFS return {}; } - std::filesystem::path Manager::getAbsoluteFileName(const std::filesystem::path& name) const + std::filesystem::file_time_type Manager::getLastModified(VFS::Path::NormalizedView name) const { - std::string normalized = Files::pathToUnicodeString(name); - Path::normalizeFilenameInPlace(normalized); - - const auto found = mIndex.find(normalized); + const auto found = mIndex.find(name); if (found == mIndex.end()) - throw std::runtime_error("Resource '" + normalized + "' is not found"); - return found->second->getPath(); + throw std::runtime_error("Resource '" + std::string(name.value()) + "' not found"); + return found->second->getLastModified(); + } + + std::string Manager::getStem(VFS::Path::NormalizedView name) const + { + const auto found = mIndex.find(name); + if (found == mIndex.end()) + throw std::runtime_error("Resource '" + std::string(name.value()) + "' not found"); + return found->second->getStem(); } RecursiveDirectoryRange Manager::getRecursiveDirectoryIterator(std::string_view path) const diff --git a/components/vfs/manager.hpp b/components/vfs/manager.hpp index b6a9d796cc..3d10b3f355 100644 --- a/components/vfs/manager.hpp +++ b/components/vfs/manager.hpp @@ -72,10 +72,9 @@ namespace VFS RecursiveDirectoryRange getRecursiveDirectoryIterator() const; - /// Retrieve the absolute path to the file - /// @note Throws an exception if the file can not be found. - /// @note May be called from any thread once the index has been built. - std::filesystem::path getAbsoluteFileName(const std::filesystem::path& name) const; + std::filesystem::file_time_type getLastModified(VFS::Path::NormalizedView name) const; + // Equivalent to std::filesystem::path::stem. The result isn't normalized. + std::string getStem(VFS::Path::NormalizedView name) const; private: std::vector> mArchives; diff --git a/components/vfs/pathutil.hpp b/components/vfs/pathutil.hpp index f5393617d7..a890be8a54 100644 --- a/components/vfs/pathutil.hpp +++ b/components/vfs/pathutil.hpp @@ -127,6 +127,27 @@ namespace VFS::Path return stream << value.mValue; } + NormalizedView parent() const + { + NormalizedView p; + const std::size_t pos = mValue.find_last_of(separator); + if (pos != std::string_view::npos) + p.mValue = mValue.substr(0, pos); + return p; + } + + std::string_view stem() const + { + std::string_view stem = mValue; + std::size_t pos = stem.find_last_of(separator); + if (pos != std::string_view::npos) + stem = stem.substr(pos + 1); + pos = stem.find_first_of(extensionSeparator); + if (pos != std::string_view::npos) + stem = stem.substr(0, pos); + return stem; + } + private: std::string_view mValue; }; @@ -259,6 +280,16 @@ namespace VFS::Path return stream << value.mValue; } + NormalizedView parent() const + { + return NormalizedView(*this).parent(); + } + + std::string_view stem() const + { + return NormalizedView(*this).stem(); + } + private: std::string mValue; }; diff --git a/components/vfs/registerarchives.cpp b/components/vfs/registerarchives.cpp index f017b5f73c..1d03766220 100644 --- a/components/vfs/registerarchives.cpp +++ b/components/vfs/registerarchives.cpp @@ -14,7 +14,7 @@ namespace VFS { void registerArchives(VFS::Manager* vfs, const Files::Collections& collections, - const std::vector& archives, bool useLooseFiles) + const std::vector& archives, bool useLooseFiles, const ToUTF8::StatelessUtf8Encoder* encoder) { const Files::PathContainer& dataDirs = collections.getPaths(); @@ -25,7 +25,7 @@ namespace VFS // Last BSA has the highest priority const auto archivePath = collections.getPath(*archive); Log(Debug::Info) << "Adding BSA archive " << archivePath; - vfs->addArchive(makeBsaArchive(archivePath)); + vfs->addArchive(makeBsaArchive(archivePath, encoder)); } else { diff --git a/components/vfs/registerarchives.hpp b/components/vfs/registerarchives.hpp index dac29f87a3..03247b74d3 100644 --- a/components/vfs/registerarchives.hpp +++ b/components/vfs/registerarchives.hpp @@ -3,13 +3,18 @@ #include +namespace ToUTF8 +{ + class StatelessUtf8Encoder; +} + namespace VFS { class Manager; /// @brief Register BSA and file system archives based on the given OpenMW configuration. void registerArchives(VFS::Manager* vfs, const Files::Collections& collections, - const std::vector& archives, bool useLooseFiles); + const std::vector& archives, bool useLooseFiles, const ToUTF8::StatelessUtf8Encoder* encoder); } #endif diff --git a/docs/source/_ext/omw-lexers.py b/docs/source/_ext/omw-lexers.py new file mode 100644 index 0000000000..02cd526304 --- /dev/null +++ b/docs/source/_ext/omw-lexers.py @@ -0,0 +1,19 @@ +from pygments.lexer import RegexLexer, bygroups +from pygments.token import Comment, Name, Operator, String, Text +from sphinx.highlighting import lexers + +class OMWConfigLexer(RegexLexer): + name = 'openmwcfg' + aliases = ['openmwcfg'] + filenames = ['openmw.cfg'] + + tokens = { + 'root': [ + (r'(\s*)(#.*$)', bygroups(Text.Whitespace, Comment.Single)), + (r'(\s*)([a-zA-Z0-9_.+-]+)(\s*(\+)?=\s*)(.*)', bygroups(Text.Whitespace, Name.Attribute, Operator, Operator, String)), + (r'.+\n', Text), + ], + } + +def setup(_): + lexers["openmwcfg"] = OMWConfigLexer() diff --git a/docs/source/_static/luadoc.css b/docs/source/_static/luadoc.css index b2efec85b4..96b7de9bb3 100644 --- a/docs/source/_static/luadoc.css +++ b/docs/source/_static/luadoc.css @@ -20,6 +20,10 @@ white-space: normal; } +html:has(#luadoc) { + scroll-padding-top: 4.0rem; +} + #content #luadoc code a:not(.toc-backref) { font-weight: 600; } diff --git a/docs/source/_static/theme-override.css b/docs/source/_static/theme-override.css index e41ab2c2d6..25f7bd6f10 100644 --- a/docs/source/_static/theme-override.css +++ b/docs/source/_static/theme-override.css @@ -36,8 +36,8 @@ --muted: 223 27% 14%; --card: 220 14% 9%; - --link: #ffffff; - --link-hover: #ffffff; + --link: #95b1dd; + --link-hover: #dde2eb; } .contents ul li a.reference:hover, .sd-dropdown .toctree-wrapper ul li a.reference, details.sd-dropdown .sd-summary-title { @@ -138,3 +138,21 @@ tbody tr:hover { #left-sidebar { overflow-y: scroll; } + +#content div[class^=highlight], #content pre.literal-block, p, h4, h5, h6 { + margin-bottom: 1.5rem; +} + +#content table p { + margin-bottom: 0; +} + +h5 { + font-size: 1.15rem; + font-weight: 600; +} + +h6 { + font-size: 1.08rem; + font-weight: 600; +} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index aaa42e3678..0b953a4275 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -43,7 +43,8 @@ extensions = [ 'sphinx.ext.viewcode', 'sphinx.ext.autosectionlabel', 'sphinx_design', - 'omw-directives' + 'omw-directives', + 'omw-lexers', ] #autosectionlabel_prefix_document = True @@ -138,7 +139,7 @@ exclude_patterns = [] #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'default' pygments_style_dark = 'github-dark' # A list of ignored prefixes for module index sorting. diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 997d5846af..6ad2ec90cc 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -2,6 +2,7 @@ paths=( openmw_aux/*lua scripts/omw/activationhandlers.lua scripts/omw/ai.lua + scripts/omw/combat/local.lua scripts/omw/input/playercontrols.lua scripts/omw/mechanics/animationcontroller.lua scripts/omw/input/gamepadcontrols.lua diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index cb44f7d1de..0dad7bfa4d 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -81,15 +81,15 @@ For Distributions Using `apt` (e.g., Ubuntu, Debian) .. code:: console - sudo apt update - sudo apt install innoextract + $ sudo apt update + $ sudo apt install innoextract For macOS using Homebrew ++++++++++++++++++++++++ .. code:: console - brew install innoextract + $ brew install innoextract Once innoextract is installed, download the game from GOG. The downloaded file should be called ``setup_tes_morrowind_goty_2.0.0.7.exe`` or something similar. When ``innoextract`` is run on it, it will extract the files directly into the folder the ``setup.exe`` file is located. If you have a specific folder where you want it to be extracted to, for example in ``~/Documents/Games/Morrowind`` You can specify it with the ``-d`` flag. diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index c99b04da91..05ea40a638 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -62,6 +62,25 @@ However, it depends on several packages which are not in stable, so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. This is not recommended or supported. +Fedora +====== + +OpenMW is available in the official repository of Fedora for versions 41 and up. +To install simply run the following as root (or in sudo), depending on what packages +you want. + +``openmw`` includes the launcher, install wizard, iniimporter and the engine itself. + +``openmw-cs`` includes the construction set. + +``openmw-tools`` includes ``bsatool``, ``esmtool`` and ``niftest``. + +.. code-block:: console + + $ dnf install openmw + $ dnf install openmw-cs + $ dnf install openmw-tools + Flatpak ======= diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index 29b14aee55..cc1e45403f 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -28,7 +28,7 @@ Engine handler is a function defined by a script, that can be called by the engi | `assigned to a script in openmw-cs (not yet implemented).` | ``onInterfaceOverride`` can be called before ``onInit``. * - onUpdate(dt) - - | Called every frame if the game is not paused. `dt` is + - | Called every frame in the Lua thread (even if the game is paused). `dt` is | the simulation time from the last update in seconds. * - onSave() -> savedData - | Called when the game is saving. May be called in inactive state, diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 007e0e43d1..590ff14d88 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -41,9 +41,112 @@ Example: core.sendGlobalEvent('UseItem', {object = potion, actor = player, force = true}) +**ModifyStat** + +Modify the corresponding stat. + +.. code-block:: Lua + + -- Consume 10 magicka + actor:sendEvent('ModifyStat', {name = 'magicka', amount = -10}) + +**AddVfx** + +Calls the corresponding method in openmw.animation + +.. code-block:: Lua + + local eventParams = { + model = 'vfx_default', + options = { + textureOverride = effect.particle, + }, + } + actor:sendEvent('AddVfx', eventParams) + +**PlaySound3d** + +Calls the corresponding function in openw.core on the target. Will use core.sound.playSoundFile3d instead of core.sound.playSound3d if you put `file` instead of `sound` in the event data. + +.. code-block:: Lua + actor:sendEvent('PlaySound3d', {sound = 'Open Lock'}) + +**BreakInvisibility** + +Forces the actor to lose all active invisibility effects. + +**Unequip** + +Any script can send ``Unequip`` events with the argument ``item`` or ``slot`` to any actor, to make that actor unequip an item. + +The following two examples are equivalent, except the ``item`` variant is guaranteed to only unequip the specified item and won't unequip a different item if the actor's equipment changed during the same frame: + +.. code-block:: Lua + + local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft) + if item then + actor:sendEvent('Unequip', {item = item}) + end + +.. code-block:: Lua + + actor:sendEvent('Unequip', {slot = Actor.EQUIPMENT_SLOT.CarriedLeft}) + +Combat events +------------- + +**Hit** + +Any script can send ``Hit`` events with arguments described in the Combat interface to cause a hit to an actor + +Example: + +.. code-block:: Lua + + -- See Combat#AttackInfo + local attack = { + attacker = self, + weapon = Actor.getEquipment(self, Actor.EQUIPMENT_SLOT.CarriedRight), + sourceType = I.Combat.ATTACK_SOURCE_TYPE.Melee, + strenght = 1, + type = self.ATTACK_TYPE.Chop, + damage = { + health = 20, + fatigue = 10, + }, + successful = true, + } + victim:sendEvent('Hit', attack) + +Item events +----------- + +**ModifyItemCondition** + +Any script can send ``ModifyItemCondition`` events to global to adjust the condition of an item. + +Example: + +.. code-block:: Lua + + local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft) + if item then + -- Reduce condition by 1 + -- Note that actor should be included, if applicable, to allow forcibly unequipping items whose condition is reduced to 0 + core:sendGlobalEvent('ModifyItemCondition', {actor = self, item = item, amount: -1}) + end + UI events --------- +**ShowMessage** + +If sent to a player, shows a message as if a call to ui.showMessage was made. + +.. code-block:: Lua + + player:sendEvent('ShowMessage', {message = 'Lorem ipsum'}) + **UiModeChanged** Every time UI mode is changed built-in scripts send to player the event ``UiModeChanged`` with arguments ``oldMode, ``newMode`` (same as ``I.UI.getMode()``) @@ -91,3 +194,36 @@ Global events that just call the corresponding function in `openmw.world`. -- world.setSimulationTimeScale(scale) core.sendGlobalEvent('SetSimulationTimeScale', scale) + + +**SpawnVfx, PlaySound3d** + +Calls the corresponding function in openw.core. Note that PlaySound3d will call core.sound.playSoundFile3d instead of core.sound.playSound3d if you put `file` instead of `sound` in the event data. + +.. code-block:: Lua + core.sendGlobalEvent('SpawnVfx', {position = hitPos, model = 'vfx_destructarea', options = {scale = 10}}) + core.sendGlobalEvent('PlaySound3d', {sound = 'Open Lock', position = container.position}) + +**ConsumeItem** + +Reduces stack size of an item by a given amount, removing the item completely if stack size is reduced to 0 or less. + +.. code-block:: Lua + + core.sendGlobalEvent('ConsumeItem', {item = foobar, amount = 1}) + +**Lock** + +Lock a container or door + +.. code-block:: Lua + + core.sendGlobalEvent('Lock', {taret = selected, magnitude = 50}) + +**Unlock** + +Unlock a container or door + +.. code-block:: Lua + + core.sendGlobalEvent('Unlock', {taret = selected}) diff --git a/docs/source/reference/lua-scripting/interface_combat.rst b/docs/source/reference/lua-scripting/interface_combat.rst new file mode 100644 index 0000000000..e3175ea27c --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_combat.rst @@ -0,0 +1,8 @@ +Interface Combat +================ + +.. include:: version.rst + +.. raw:: html + :file: generated_html/scripts_omw_combat_local.html + diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 852d63ca0a..b838385524 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -126,10 +126,12 @@ The options are: Enable it in ``openmw.cfg`` the same way as any other mod: -:: +.. code-block:: openmwcfg + :caption: openmw.cfg data=path/to/my_lua_mod - content=my_lua_mod.omwscripts # or content=my_lua_mod.omwaddon + # or content=my_lua_mod.omwaddon + content=my_lua_mod.omwscripts Now every time the player presses "X" on a keyboard, a message is shown. diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index 8496d01029..562666c85e 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -23,6 +23,9 @@ * - :doc:`Crimes ` - |bdg-ctx-global| - Commit crimes. + * - :doc:`Combat ` + - |bdg-ctx-local| + - Control combat of NPCs and creatures * - :doc:`GamepadControls ` - |bdg-ctx-player| - Allows to alter behavior of the built-in script that handles player gamepad controls. diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-collada-animated-creature.rst b/docs/source/reference/modding/custom-models/pipeline-blender-collada-animated-creature.rst index f07ba7651a..21da034a54 100644 --- a/docs/source/reference/modding/custom-models/pipeline-blender-collada-animated-creature.rst +++ b/docs/source/reference/modding/custom-models/pipeline-blender-collada-animated-creature.rst @@ -177,7 +177,6 @@ definitions and events. At a minimum it needs to include at least animation runforward: stop 4.433333 attack1: start 4.466667 attack1: stop 5.433333 - ... The textkeys file is placed in the same folder as the model and matches the model's name. diff --git a/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst b/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst index d52c952b3d..5fb109c962 100644 --- a/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst +++ b/docs/source/reference/modding/custom-models/pipeline-blender-collada-static-models.rst @@ -168,7 +168,7 @@ the file path to the texture is incorrect and OpenMW can't find it. To fix this you can open the exported ``.dae`` file in a text editor and check the texture's filepath. In the example of this barrel model it's found on lines 13-17. -.. code:: +.. code-block:: xml diff --git a/docs/source/reference/modding/custom-shader-effects.rst b/docs/source/reference/modding/custom-shader-effects.rst index 0e7776c7ae..41dd4c7369 100644 --- a/docs/source/reference/modding/custom-shader-effects.rst +++ b/docs/source/reference/modding/custom-shader-effects.rst @@ -23,7 +23,8 @@ dungeons. To use this feature the :ref:`soft particles` setting must be enabled. This setting can either be activated in the OpenMW launcher or changed in `settings.cfg`: -:: +.. code-block:: ini + :caption: settings.cfg [Shaders] soft particles = true @@ -64,7 +65,8 @@ Blue and alpha channels are ignored. To use this feature the :ref:`post processing ` setting must be enabled. This setting can either be activated in the OpenMW launcher, in-game, or changed in `settings.cfg`: -:: +.. code-block:: ini + :caption: settings.cfg [Post Processing] enabled = true diff --git a/docs/source/reference/modding/extended.rst b/docs/source/reference/modding/extended.rst index 001492f254..8b5c30fdd0 100644 --- a/docs/source/reference/modding/extended.rst +++ b/docs/source/reference/modding/extended.rst @@ -91,7 +91,8 @@ The behavior of such a model: The actual state toggling time depends on the sunrise/sunset time settings in `openmw.cfg`: -:: +.. code-block:: openmwcfg + :caption: openmw.cfg fallback=Weather_Sunrise_Time,6 fallback=Weather_Sunset_Time,18 @@ -102,7 +103,8 @@ These settings lead to the "night" starting at 20:00 and ending at 6:00. The engine checks if the weather is bright enough to support the "interior day" mode using the Glare_View setting. If it is >= 0.5, the engine considers the weather bright. -:: +.. code-block:: openmwcfg + :caption: openmw.cfg fallback=Weather_Clear_Glare_View,1 fallback=Weather_Foggy_Glare_View,0.25 @@ -138,7 +140,8 @@ If you want to override walking animations, you should override ``xbase_anim_fem To enable this feature, you should have this line in your settings.cfg: -:: +.. code-block:: ini + :caption: settings.cfg [Game] use additional anim sources = true @@ -157,7 +160,8 @@ This feature conflicts with old mods which use scripted scabbards, arrows with p The minimum you need is the ``xbase_anim_sh.nif`` file from the `Weapon Sheathing`_ mod and this line in your settings.cfg: -:: +.. code-block:: ini + :caption: settings.cfg [Game] weapon sheathing = true @@ -205,7 +209,8 @@ Skeleton extensions It is possible to inject custom bones into actor skeletons: -:: +.. code-block:: ini + :caption: settings.cfg [Game] use additional anim sources = true @@ -323,14 +328,16 @@ General advices to create assets for this feature: Groundcover mods can be registered in the openmw.cfg via "groundcover" entries instead of "content" ones: -:: +.. code-block:: openmwcfg + :caption: openmw.cfg groundcover=my_grass_mod.esp Every static from such mod is treated as a groundcover object. Also groundcover detection should be enabled via settings.cfg: -:: +.. code-block:: ini + :caption: settings.cfg [Groundcover] enabled = true diff --git a/docs/source/reference/modding/font.rst b/docs/source/reference/modding/font.rst index 2c67a940d1..c0e91eec04 100644 --- a/docs/source/reference/modding/font.rst +++ b/docs/source/reference/modding/font.rst @@ -3,8 +3,11 @@ Fonts Default UI font and font used in magic scrolls are defined in ``openmw.cfg``: - fallback=Fonts_Font_0,MysticCards - fallback=Fonts_Font_2,DemonicLetters +.. code-block:: openmwcfg + :caption: openmw.cfg + + fallback=Fonts_Font_0,MysticCards + fallback=Fonts_Font_2,DemonicLetters When there are no ``Fonts_Font_*`` lines in user's ``openmw.cfg``, built-in TrueType fonts are used. Font used by console and another debug windows is not configurable (so ``Fonts_Font_1`` is unused). @@ -20,8 +23,11 @@ You can use --export-fonts command line option to write the converted font They can be used instead of TrueType fonts if needed by specifying their ``.fnt`` files names in the ``openmw.cfg``. For example: - fallback=Fonts_Font_0,magic_cards_regular - fallback=Fonts_Font_2,daedric_font +.. code-block:: openmwcfg + :caption: openmw.cfg + + fallback=Fonts_Font_0,magic_cards_regular + fallback=Fonts_Font_2,daedric_font In this example OpenMW will search for ``magic_cards_regular.fnt`` and ``daedric_font.fnt`` in the ``Fonts`` folder in data directories. If they are not found, built-in TrueType fonts will be used as a fallback. @@ -35,16 +41,22 @@ Unlike vanilla Morrowind, OpenMW directly supports TrueType (``.ttf``) fonts. Th OpenMW has build-in TrueType fonts: MysticCards, DemonicLetters and DejaVuLGCSansMono, which are used by default. TrueType fonts are configured via ``openmw.cfg`` too: - fallback=Fonts_Font_0,MysticCards - fallback=Fonts_Font_2,DemonicLetters +.. code-block:: openmwcfg + :caption: openmw.cfg + + fallback=Fonts_Font_0,MysticCards + fallback=Fonts_Font_2,DemonicLetters In this example, OpenMW will scan ``Fonts`` folder in data directories for ``.omwfont`` files. These files are XML files with schema provided by MyGUI. OpenMW uses ``.omwfont`` files which name (without extension) matches ``openmw.cfg`` entries. -It is also possible to adjust the font size via ``settings.cfg`` file:: +It is also possible to adjust the font size via ``settings.cfg`` file: - [GUI] - font size = 16 +.. code-block:: ini + :caption: settings.cfg + + [GUI] + font size = 16 The ``font size`` setting accepts clamped values in range from 12 to 18. diff --git a/docs/source/reference/modding/openmw-game-template.rst b/docs/source/reference/modding/openmw-game-template.rst index 68fa667911..51ddaf6efb 100644 --- a/docs/source/reference/modding/openmw-game-template.rst +++ b/docs/source/reference/modding/openmw-game-template.rst @@ -36,7 +36,8 @@ and ``data=`` tells OpenMW what folders to look for meshes, textures, audio, and other assets. The required lines would look like this, but with the paths of course different on your system. -.. code:: +.. code-block:: openmwcfg + :caption: openmw.cfg content=template.omwgame data="/home/someuser/example-suite/data" @@ -51,7 +52,8 @@ you need to remove or comment out the following lines from ``openmw.cfg``. Not doing so will either produce errors or load Morrowind content, which you probably do not want when you are making your own game. -.. code:: +.. code-block:: openmwcfg + :caption: openmw.cfg fallback-archive=Morrowind.bsa fallback-archive=Tribunal.bsa @@ -70,8 +72,10 @@ are instead assigned through ``settings.cfg``. These models are player and NPC animations, and meshes for the sky. In ``settings.cfg`` used by your OpenMW install, add the following lines under the ``[Models]`` section. -.. code:: +.. code-block:: ini + :caption: settings.cfg + [Models] xbaseanim = meshes/BasicPlayer.dae baseanim = meshes/BasicPlayer.dae xbaseanim1st = meshes/BasicPlayer.dae @@ -103,7 +107,7 @@ need to be copied to ``resources/mygui`` folder found in your OpenMW installatio folder. Overwrite any files aready in this folder. These files provide the UI font, its definition, and some minor UI tweaks. -.. code:: +.. code-block:: none openmw_box.skin.xml openmw_button.skin.xml diff --git a/docs/source/reference/modding/paths.rst b/docs/source/reference/modding/paths.rst index 4ea5219e74..c0f7d4cea6 100644 --- a/docs/source/reference/modding/paths.rst +++ b/docs/source/reference/modding/paths.rst @@ -134,7 +134,8 @@ This can't change until computers are able to read minds. Lines with options have an option name, then an equals sign (``=``), then an option value. Option names and values have leading and trailing whitespace trimmed, but whitespace within an option value is preserved - it's only removed if it's at the ends. This means that these are all equivalent: -:: + +.. code-block:: openmwcfg data=some/dir data=some/dir @@ -226,7 +227,10 @@ Navigate to the OpenMW installation directory, and open the ``openmw.cfg`` file By default, this contains a warning at the top telling you that this is the local ``openmw.cfg`` and not to modify it. However, for this kind of install, it's okay to do so, so you can remove this warning. -Change the start of the file from:: +Change the start of the file from: + +.. code-block:: openmwcfg + :caption: openmw.cfg # This is the local openmw.cfg file. Do not modify! # Modifications should be done on the user openmw.cfg file instead @@ -243,7 +247,10 @@ Change the start of the file from:: fallback=LightAttenuation_ConstantValue,0.0 fallback=LightAttenuation_UseLinear,1 -to:: +to: + +.. code-block:: openmwcfg + :caption: openmw.cfg data-local=userdata/data user-data=userdata @@ -274,7 +281,10 @@ Navigate to the OpenMW installation directory, and open the ``openmw.cfg`` file By default, this contains a warning at the top telling you that this is the local ``openmw.cfg`` and not to modify it. However, you'll need to make a small change to create this kind of install. -Change the start of the file from:: +Change the start of the file from: + +.. code-block:: openmwcfg + :caption: openmw.cfg # This is the local openmw.cfg file. Do not modify! # Modifications should be done on the user openmw.cfg file instead @@ -291,7 +301,10 @@ Change the start of the file from:: fallback=LightAttenuation_ConstantValue,0.0 fallback=LightAttenuation_UseLinear,1 -to:: +to: + +.. code-block:: openmwcfg + :caption: openmw.cfg # This is the local openmw.cfg file. Do not modify! # Modifications should be done on the user openmw.cfg file instead @@ -330,7 +343,10 @@ From scratch Start by installing OpenMW in the usual way. Don't bother with first-time setup (i.e. telling it the location of an existing *Morrowind* installation). -In the default configuration directory (see `Configuration files and log files`_), create a file called ``openmw.cfg`` containing just:: +In the default configuration directory (see `Configuration files and log files`_), create a file called ``openmw.cfg`` containing just + +.. code-block:: openmwcfg + :caption: openmw.cfg # select the game profile config=Morrowind @@ -340,7 +356,10 @@ This will put the basic setup required to play *Morrowind* into a new ``Morrowin Next, come up with a name for the subprofile you'll create for your mod list. If you're following a modding guide, they've probably already given it a name, e.g. *Total Overhaul*, so that's the example we'll use. -Add a line to the ``Morrowind/openmw.cfg`` with the profile name, e.g.:: +Add a line to the ``Morrowind/openmw.cfg`` with the profile name like this: + +.. code-block:: openmwcfg + :caption: Morrowind/openmw.cfg # select the mod list profile config=Total Overhaul @@ -356,7 +375,10 @@ The ones in the ``Morrowind`` directory are used for all profiles for *Morrowind The ones in the ``Morrowind/Total Overhaul`` directory are only used for the *Total Overhaul* profile, so you can set up that mod list and any settings it requires here, and they won't affect any other profiles you set up later. Making changes within the launcher will affect these files and leave all the others alone. -If you want the *Total Overhaul* profile to keep its saved games etc. in a dedicated location instead of mixing them in with ones from another profile, you can add a ``user-data=…`` line to your ``Morrowind/Total Overhaul/openmw.cfg``, e.g.:: +If you want the *Total Overhaul* profile to keep its saved games etc. in a dedicated location instead of mixing them in with ones from another profile, you can add a ``user-data=…`` line to your ``Morrowind/Total Overhaul/openmw.cfg``, like this: + +.. code-block:: openmwcfg + :caption: Morrowind/Total Overhaul/openmw.cfg # put saved games in a saves directory next to this file user-data=. @@ -377,12 +399,18 @@ You'll now have an empty directory e.g. at ``Documents\My Games\OpenMW\Original` Next, move all the files that were already in the default configuration directory to the profile directory you just made. Afterwards, the default configuration directory should only contain the profile directory you made. -Create a new ``openmw.cfg`` file in the default configuration directory containing:: +Create a new ``openmw.cfg`` file in the default configuration directory containing: + +.. code-block:: openmwcfg + :caption: openmw.cfg # select the profile config=Original -In the ``openmw.cfg`` in the profile directory, add these lines:: +In the ``openmw.cfg`` in the profile directory, add these lines: + +.. code-block:: openmwcfg + :caption: openmw.cfg data-local=data user-data=. @@ -402,9 +430,11 @@ Passing arguments on the command line lets you avoid this. The basic idea is that you need to pass ``--replace config`` to ignore the configuration directories that the engine would have loaded because they were specified in ``openmw.cfg`` files, and pass each one you want to use instead with ``--config ``. -E.g. if you've got a profile called *Morrowind* in your default configuration directory, and it's got a *Total Overhaul* subprofile, you could load it by running:: +E.g. if you've got a profile called *Morrowind* in your default configuration directory, and it's got a *Total Overhaul* subprofile, you could load it by running: - openmw --replace config --config ?userconfig?/Morrowind --config "?userconfig?/Morrowind/Total Overhaul" +.. code-block:: console + + $ openmw --replace config --config ?userconfig?/Morrowind --config "?userconfig?/Morrowind/Total Overhaul" You can put this command into a script or shortcut and use it to easily launch OpenMW with that profile. @@ -422,15 +452,17 @@ On Windows, you can create a desktop shortcut to run this command with these ste * At the end of that field, add the arguments for the profile you want, e.g. ``--replace config --config ?userconfig?/Morrowind --config "?userconfig?/Morrowind/Total Overhaul"``. * Press *Apply* or *OK* to save the changes, and test the shortcut by double-clicking it. -On most Linux distros, you can create a ``.desktop`` file like this:: +On most Linux distros, you can create a ``.desktop`` file like this: - [Desktop Entry] - Type=Application - Name=OpenMW - Total Overhaul - GenericName=Role Playing Game - Comment=OpenMW with the Total Overhaul profile - Keywords=Morrowind;Reimplementation Mods;esm;bsa; - TryExec=openmw - Exec=openmw --replace config --config ?userconfig?/Morrowind --config "?userconfig?/Morrowind/Total Overhaul" - Icon=openmw - Categories=Game;RolePlaying; +.. code-block:: desktop + + [Desktop Entry] + Type=Application + Name=OpenMW - Total Overhaul + GenericName=Role Playing Game + Comment=OpenMW with the Total Overhaul profile + Keywords=Morrowind;Reimplementation Mods;esm;bsa; + TryExec=openmw + Exec=openmw --replace config --config ?userconfig?/Morrowind --config "?userconfig?/Morrowind/Total Overhaul" + Icon=openmw + Categories=Game;RolePlaying; diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst index b9065c8daf..7dc38f8c39 100644 --- a/docs/source/reference/modding/settings/GUI.rst +++ b/docs/source/reference/modding/settings/GUI.rst @@ -77,17 +77,16 @@ GUI Settings :type: boolean :range: true, false :default: true - + Enables or disables the red flash overlay when the character takes damage. - Disabling causes the player to "bleed" like NPCs. .. omw-setting:: :title: werewolf overlay :type: boolean :range: true, false :default: true - + Enable or disable the werewolf visual effect in first-person mode. @@ -96,7 +95,7 @@ GUI Settings :type: color :range: [0, 1] :default: 0.15 0.0 0.0 1.0 - + Background color of tooltip and crosshair when hovering over an NPC-owned item. Four floating point values: red, green, blue, alpha (alpha ignored). @@ -107,7 +106,7 @@ GUI Settings :type: color :range: [0, 1] :default: 1.0 0.15 0.15 1.0 - + Crosshair color when hovering over an NPC-owned item. Four floating point values: red, green, blue, alpha (alpha ignored). diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 19d31279c6..fcbc8763ac 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -442,17 +442,6 @@ Game Settings Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state. -.. omw-setting:: - :title: unarmed creature attacks damage armor - :type: boolean - :range: true, false - :default: false - :location: :bdg-success:`Launcher > Settings > Gameplay` - - If disabled unarmed creature attacks do not reduce armor condition, just as with vanilla engine. - - If enabled unarmed creature attacks reduce armor condition, the same as attacks from NPCs and armed creatures. - .. omw-setting:: :title: actor collision shape type :type: int diff --git a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst index e05177c268..89ea25519d 100644 --- a/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst +++ b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst @@ -2,24 +2,9 @@ Normal maps from Morrowind to OpenMW ==================================== -- `General introduction to normal map conversion`_ - - `OpenMW normal-mapping`_ - - `Activating normal-mapping shaders in OpenMW`_ - - `Morrowind bump-mapping`_ - - `MGE XE normal-mapping`_ -- `Converting PeterBitt's Scamp Replacer`_ (Mod made for the MGE XE PBR prototype) - - `Tutorial - MGE`_ -- `Converting Lougian's Hlaalu Bump mapped`_ (Morrowind's bump-mapping, part 1: *without* custom models) - - `Tutorial - Morrowind, Part 1`_ -- `Converting Apel's Various Things - Sacks`_ (Morrowind's bump-mapping, part 2: *with* custom models) - - `Tutorial - Morrowind, Part 2`_ - General introduction to normal map conversion --------------------------------------------- -:Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov -:Updated: 2020-03-03 - This page has general information and tutorials on how normal-mapping works in OpenMW and how you can make mods using the old environment-mapped bump-mapping technique (such as `Netch Bump mapped`_ and `Hlaalu Bump mapped`_, and maybe the most (in)famous one to previously give shiny rocks in OpenMW, the mod `On the Rocks`_!, featured in MGSO and Morrowind Rebirth) work better in OpenMW. @@ -58,14 +43,15 @@ Activating normal-mapping shaders in OpenMW Before normal (and specular and parallax) maps can show up in OpenMW, their auto-detection needs to be turned on in settings.cfg_-file. Add these rows where it would make sense: -:: +.. code-block:: ini + :caption: settings.cfg - [Shaders] - auto use object normal maps = true - auto use terrain normal maps = true + [Shaders] + auto use object normal maps = true + auto use terrain normal maps = true - auto use object specular maps = true - auto use terrain specular maps = true + auto use object specular maps = true + auto use terrain specular maps = true See OpenMW's wiki page about `texture modding`_ to read more about it. @@ -81,10 +67,11 @@ are processed which makes bump-mapped models look a bit better, can make use of the gloss map channel in the bump map and can apply bump-mapping to skinned models. Add this to settings.cfg_-file: -:: +.. code-block:: ini + :caption: settings.cfg - [Shaders] - apply lighting to environment maps = true + [Shaders] + apply lighting to environment maps = true But sometimes you may want them to look a bit better than in vanilla. Technically you aren't supposed to convert bump maps because they shouldn't be normal maps that are supported by OpenMW as well, @@ -117,9 +104,6 @@ Converting PeterBitt's Scamp Replacer ------------------------------------- **Mod made for the MGE XE PBR prototype** -:Authors: Joakim (Lysol) Berg -:Updated: 2016-11-11 - So, let's say you've found out that PeterBitt_ makes awesome models and textures featuring physically based rendering (PBR) and normal maps. Let's say that you tried to run his `PBR Scamp Replacer`_ in OpenMW and that you were greatly disappointed when the normal map didn't seem to work. Lastly, let's say you came here, looking for some answers. @@ -161,9 +145,6 @@ Converting Lougian's Hlaalu Bump mapped --------------------------------------- **Mod made for Morrowind's bump-mapping, without custom models** -:Authors: Joakim (Lysol) Berg, Alexei (Capo) Dobrohotov -:Updated: 2020-03-03 - Converting normal maps made for the Morrowind's bump-mapping can be really easy or a real pain, depending on a few circumstances. In this tutorial, we will look at a very easy, although in some cases a bit time-consuming, example. @@ -192,9 +173,6 @@ Converting Apel's Various Things - Sacks ---------------------------------------- **Mod made for Morrowind bump-mapping, with custom models** -:Authors: Joakim (Lysol) Berg, Alexei (Capostrophic) Dobrohotov -:Updated: 2020-03-03 - In part one of this tutorial, we converted a mod that only included modified Morrowind model (``.nif``) files so that the bump maps could be loaded as normal maps. We ignored those model files since they are not needed with OpenMW. In this tutorial however, diff --git a/docs/source/reference/modding/texture-modding/texture-basics.rst b/docs/source/reference/modding/texture-modding/texture-basics.rst index 96ec48508b..26a47f4e0a 100644 --- a/docs/source/reference/modding/texture-modding/texture-basics.rst +++ b/docs/source/reference/modding/texture-modding/texture-basics.rst @@ -55,17 +55,20 @@ Simply create the textures with appropriate naming convention the normal map would have to be called foo_n.dds). To enable this automatic use based on filename pattern, you will have to add the following to your -`settings.cfg `_ file:: +`settings.cfg `_ file: - [Shaders] - auto use object normal maps = true +.. code-block:: ini + :caption: settings.cfg - auto use object specular maps = true + [Shaders] + auto use object normal maps = true - normal map pattern = _n - normal height map pattern = _nh + auto use object specular maps = true - specular map pattern = _spec + normal map pattern = _n + normal height map pattern = _nh + + specular map pattern = _spec Additionally, a normal map with the `_nh` pattern enables the use of the normal map's alpha channel as height information. @@ -92,18 +95,21 @@ For example, if you wanted to add specular mapping to a terrain layer called roc you would copy this texture to a new file called rock_diffusespec.dds, and then edit its alpha channel to set the specular intensity. -The relevant settings are:: +The relevant settings are - [Shaders] - auto use terrain normal maps = true +.. code-block:: ini + :caption: settings.cfg - auto use terrain specular maps = true + [Shaders] + auto use terrain normal maps = true - terrain specular map pattern = _diffusespec + auto use terrain specular maps = true - # Also used for terrain normal maps - normal map pattern = _n - normal height map pattern = _nh + terrain specular map pattern = _diffusespec + + # Also used for terrain normal maps + normal map pattern = _n + normal height map pattern = _nh OSG native files ################ diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt index 74ba957813..ef39dc20e1 100644 --- a/extern/CMakeLists.txt +++ b/extern/CMakeLists.txt @@ -209,6 +209,7 @@ if (BUILD_BENCHMARKS AND NOT OPENMW_USE_SYSTEM_BENCHMARK) set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_INSTALL OFF) set(BENCHMARK_ENABLE_GTEST_TESTS OFF) + set(BENCHMARK_ENABLE_WERROR OFF) include(FetchContent) FetchContent_Declare(benchmark diff --git a/extern/osgQt/CMakeLists.txt b/extern/osgQt/CMakeLists.txt index 781305ca47..1cfb8230a3 100644 --- a/extern/osgQt/CMakeLists.txt +++ b/extern/osgQt/CMakeLists.txt @@ -17,11 +17,7 @@ set(OSGQT_SOURCE_FILES add_library(${OSGQT_LIBRARY} STATIC ${OSGQT_SOURCE_FILES}) -if (QT_VERSION_MAJOR VERSION_EQUAL 6) - target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets) -else() - target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL) -endif() +target_link_libraries(${OSGQT_LIBRARY} Qt::Core Qt::OpenGL Qt::OpenGLWidgets) link_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/extern/sol3/README.md b/extern/sol3/README.md index 202b2ca08b..fe249704c4 100644 --- a/extern/sol3/README.md +++ b/extern/sol3/README.md @@ -1,5 +1,8 @@ -The code in this directory is copied from https://github.com/ThePhD/sol2.git (64096348465b980e2f1d0e5ba9cbeea8782e8f27) +The code in this directory is copied from https://github.com/ThePhD/sol2.git (c1f95a773c6f8f4fde8ca3efe872e7286afe4444) and has been patched to include -Additional changes include cherry-picking upstream commit d805d027e0a0a7222e936926139f06e23828ce9f to fix compilation under Clang 19. +https://github.com/ThePhD/sol2/pull/1674 (71d85143ad69164f5f52c3bdab91fb503c676eb4) +https://github.com/ThePhD/sol2/pull/1676 (a6872ef46b08704b9069ebf83161f4637459ce63) +https://github.com/ThePhD/sol2/pull/1716 (5b6881ed94c795298eae72b6848308e9a37e42c5) +https://github.com/ThePhD/sol2/pull/1722 (ab874eb0e8ef8aea4c10074a89efa25f62a29d9a) License: MIT diff --git a/extern/sol3/sol/abort.hpp b/extern/sol3/sol/abort.hpp new file mode 100644 index 0000000000..692244daa7 --- /dev/null +++ b/extern/sol3/sol/abort.hpp @@ -0,0 +1,47 @@ +// sol2 + +// The MIT License (MIT) + +// Copyright (c) 2013-2022 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. + +#ifndef SOL_ABORT_HPP +#define SOL_ABORT_HPP + +#include + +#include + +#include + +// clang-format off +#if SOL_IS_ON(SOL_DEBUG_BUILD) + #if SOL_IS_ON(SOL_COMPILER_VCXX) + #define SOL_DEBUG_ABORT() \ + if (true) { ::std::abort(); } \ + static_assert(true, "") + #else + #define SOL_DEBUG_ABORT() ::std::abort() + #endif +#else + #define SOL_DEBUG_ABORT() static_assert(true, "") +#endif +// clang-format on + +#endif // SOL_ABORT_HPP diff --git a/extern/sol3/sol/as_args.hpp b/extern/sol3/sol/as_args.hpp index 5afe78b0e4..719a3cdc99 100644 --- a/extern/sol3/sol/as_args.hpp +++ b/extern/sol3/sol/as_args.hpp @@ -2,7 +2,7 @@ // The MIT License (MIT) -// Copyright (c) 2013-2021 Rapptz, ThePhD and contributors +// Copyright (c) 2013-2022 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 diff --git a/extern/sol3/sol/as_returns.hpp b/extern/sol3/sol/as_returns.hpp index 0ac499e67d..982f408b71 100644 --- a/extern/sol3/sol/as_returns.hpp +++ b/extern/sol3/sol/as_returns.hpp @@ -2,7 +2,7 @@ // The MIT License (MIT) -// Copyright (c) 2013-2021 Rapptz, ThePhD and contributors +// Copyright (c) 2013-2022 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 diff --git a/extern/sol3/sol/assert.hpp b/extern/sol3/sol/assert.hpp index e46b9f122a..7f27905dc4 100644 --- a/extern/sol3/sol/assert.hpp +++ b/extern/sol3/sol/assert.hpp @@ -1,99 +1,99 @@ -// sol2 - -// The MIT License (MIT) - -// Copyright (c) 2013-2021 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. - -#pragma once - -#ifndef SOL_ASSERT_HPP -#define SOL_ASSERT_HPP - -#include - -#if SOL_IS_ON(SOL2_CI_I_) - -struct pre_main { - pre_main() { -#ifdef _MSC_VER - _set_abort_behavior(0, _WRITE_ABORT_MSG); -#endif - } -} inline sol2_ci_dont_lock_ci_please = {}; - -#endif // Prevent lockup when doing Continuous Integration - - -// clang-format off - -#if SOL_IS_ON(SOL_USER_C_ASSERT_I_) - #define sol_c_assert(...) SOL_C_ASSERT(__VA_ARGS__) -#else - #if SOL_IS_ON(SOL_DEBUG_BUILD_I_) - #include - #include - #include - - #define sol_c_assert(...) \ - do { \ - if (!(__VA_ARGS__)) { \ - std::cerr << "Assertion `" #__VA_ARGS__ "` failed in " << __FILE__ << " line " << __LINE__ << std::endl; \ - std::terminate(); \ - } \ - } while (false) - #else - #define sol_c_assert(...) \ - do { \ - if (false) { \ - (void)(__VA_ARGS__); \ - } \ - } while (false) - #endif -#endif - -#if SOL_IS_ON(SOL_USER_M_ASSERT_I_) - #define sol_m_assert(message, ...) SOL_M_ASSERT(message, __VA_ARGS__) -#else - #if SOL_IS_ON(SOL_DEBUG_BUILD_I_) - #include - #include - #include - - #define sol_m_assert(message, ...) \ - do { \ - if (!(__VA_ARGS__)) { \ - std::cerr << "Assertion `" #__VA_ARGS__ "` failed in " << __FILE__ << " line " << __LINE__ << ": " << message << std::endl; \ - std::terminate(); \ - } \ - } while (false) - #else - #define sol_m_assert(message, ...) \ - do { \ - if (false) { \ - (void)(__VA_ARGS__); \ - (void)sizeof(message); \ - } \ - } while (false) - #endif -#endif - -// clang-format on - -#endif // SOL_ASSERT_HPP +// sol2 + +// The MIT License (MIT) + +// Copyright (c) 2013-2022 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. + +#pragma once + +#ifndef SOL_ASSERT_HPP +#define SOL_ASSERT_HPP + +#include + +#if SOL_IS_ON(SOL2_CI) + +struct pre_main { + pre_main() { +#ifdef _MSC_VER + _set_abort_behavior(0, _WRITE_ABORT_MSG); +#endif + } +} inline sol2_ci_dont_lock_ci_please = {}; + +#endif // Prevent lockup when doing Continuous Integration + + +// clang-format off + +#if SOL_IS_ON(SOL_USER_ASSERT) + #define SOL_ASSERT(...) SOL_C_ASSERT(__VA_ARGS__) +#else + #if SOL_IS_ON(SOL_DEBUG_BUILD) + #include + #include + #include + + #define SOL_ASSERT(...) \ + do { \ + if (!(__VA_ARGS__)) { \ + std::cerr << "Assertion `" #__VA_ARGS__ "` failed in " << __FILE__ << " line " << __LINE__ << std::endl; \ + std::terminate(); \ + } \ + } while (false) + #else + #define SOL_ASSERT(...) \ + do { \ + if (false) { \ + (void)(__VA_ARGS__); \ + } \ + } while (false) + #endif +#endif + +#if SOL_IS_ON(SOL_USER_ASSERT_MSG) + #define SOL_ASSERT_MSG(message, ...) SOL_ASSERT_MSG(message, __VA_ARGS__) +#else + #if SOL_IS_ON(SOL_DEBUG_BUILD) + #include + #include + #include + + #define SOL_ASSERT_MSG(message, ...) \ + do { \ + if (!(__VA_ARGS__)) { \ + std::cerr << "Assertion `" #__VA_ARGS__ "` failed in " << __FILE__ << " line " << __LINE__ << ": " << message << std::endl; \ + std::terminate(); \ + } \ + } while (false) + #else + #define SOL_ASSERT_MSG(message, ...) \ + do { \ + if (false) { \ + (void)(__VA_ARGS__); \ + (void)sizeof(message); \ + } \ + } while (false) + #endif +#endif + +// clang-format on + +#endif // SOL_ASSERT_HPP diff --git a/extern/sol3/sol/base_traits.hpp b/extern/sol3/sol/base_traits.hpp index a28f23a74c..204afc276f 100644 --- a/extern/sol3/sol/base_traits.hpp +++ b/extern/sol3/sol/base_traits.hpp @@ -1,123 +1,156 @@ -// sol2 - -// The MIT License (MIT) - -// Copyright (c) 2013-2021 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. - -#ifndef SOL_BASE_TRAITS_HPP -#define 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