diff --git a/.clang-tidy b/.clang-tidy index 92500ad04d..90c72765ca 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -13,3 +13,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: 'osg(DB|FX|Particle|Shadow|Viewer|Util)?' diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 41fa39b78d..4de11d7967 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -73,7 +73,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Building Dependencies - run: CI/before_install.osx.sh + run: CI/before_install.macos.sh - name: Prime ccache uses: hendrikmuhs/ccache-action@v1 @@ -82,11 +82,9 @@ jobs: max-size: 1000M - name: Configure - run: CI/before_script.osx.sh + run: CI/before_script.macos.sh - name: Build - run: | - cd build - make -j $(sysctl -n hw.logicalcpu) package + run: CI/macos/build.sh Output-Envs: name: Read .env file and expose it as output @@ -106,7 +104,6 @@ jobs: fail-fast: true matrix: image: - - "2019" - "2022" uses: ./.github/workflows/windows.yml diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d2a7d37990..81d83451a6 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,9 +4,13 @@ on: workflow_call: inputs: image: - description: MSVC image (2019/2022) + description: Window Server image required: true type: string + msvc: + description: MSVC version (2019/2022) + default: "2022" + type: string vcpkg-deps-tag: description: Git tag of our deps required: true @@ -45,7 +49,7 @@ jobs: - name: Download prebuilt vcpkg packages working-directory: ${{ github.workspace }}/deps run: | - $MANIFEST = "vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}.txt" + $MANIFEST = "vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}.txt" curl --fail --retry 3 -L -o "$MANIFEST" "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/$MANIFEST" $lines = Get-Content "$MANIFEST" $URL = $lines[0] @@ -61,7 +65,7 @@ jobs: - name: Extract archived prebuilt vcpkg packages working-directory: ${{ github.workspace }}/deps - run: 7z x -y -ovcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }} $env:archive + run: 7z x -y -ovcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }} $env:archive - name: Cache Qt id: qt-cache @@ -94,12 +98,12 @@ jobs: -B ${{ github.workspace }}/build -G Ninja -D CMAKE_BUILD_TYPE=${{ inputs.build-type }} - -D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/scripts/buildsystems/vcpkg.cmake' + -D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/scripts/buildsystems/vcpkg.cmake' -D CMAKE_PREFIX_PATH='${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64' ${{ inputs.package && '-D CMAKE_CXX_FLAGS_RELEASE="/O2 /Ob2 /DNDEBUG /Zi"' || '' }} ${{ inputs.package && '-D "CMAKE_EXE_LINKER_FLAGS_RELEASE=/DEBUG /INCREMENTAL:NO"' || '' }} - -D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/include/luajit' - -D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/lib/lua51.lib' + -D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/include/luajit' + -D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/lib/lua51.lib' -D BUILD_BENCHMARKS=${{ ! inputs.package }} -D BUILD_COMPONENTS_TESTS=${{ ! inputs.package }} -D BUILD_OPENMW_TESTS=${{ ! inputs.package }} @@ -114,9 +118,9 @@ jobs: - name: Copy missing DLLs run: | - cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/build - cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/build - cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/build + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/build + cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/build + cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.msvc }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/build - name: Copy Qt DLLs working-directory: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 @@ -172,7 +176,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - job_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json jobs --jq '.jobs[] | select(.name == "windows-${{ inputs.image }}") | .url') + job_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json jobs --jq '.jobs[] | select(.name == "windows-${{ inputs.msvc }}") | .url') printf "Ref ${{ github.ref }}\nJob ${job_url}\nCommit ${{ github.sha }}\n" > install/CI-ID.txt cp install/CI-ID.txt pdb/CI-ID.txt cp install/CI-ID.txt SymStore/CI-ID.txt @@ -180,19 +184,19 @@ jobs: - name: Store OpenMW archived pdb files uses: actions/upload-artifact@v4 with: - name: openmw-windows-${{ inputs.image }}-pdb-${{ github.sha }} + name: openmw-windows-${{ inputs.msvc }}-pdb-${{ github.sha }} path: ${{ github.workspace }}/pdb/* - name: Store OpenMW build artifacts uses: actions/upload-artifact@v4 with: - name: openmw-windows-${{ inputs.image }}-${{ github.sha }} + name: openmw-windows-${{ inputs.msvc }}-${{ github.sha }} path: ${{ github.workspace }}/install/* - name: Store symbol server artifacts uses: actions/upload-artifact@v4 with: - name: openmw-windows-${{ inputs.image }}-sym-store-${{ github.sha }} + name: openmw-windows-${{ inputs.msvc }}-sym-store-${{ github.sha }} path: ${{ github.workspace }}/SymStore/* - name: Upload to symbol server diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d55e554905..48d9f5ff8a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,9 @@ Ubuntu_GCC_preprocess: - pip3 install --user 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,7 +80,7 @@ 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 - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}' @@ -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 @@ -268,6 +271,9 @@ 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 @@ -285,6 +291,9 @@ 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 @@ -322,6 +331,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 @@ -396,12 +408,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 +429,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,7 +445,7 @@ 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: @@ -467,7 +482,7 @@ Ubuntu_Clang_tests_Debug: stage: test variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - EXAMPLE_SUITE_REVISION: f51b832e033429a7cdc520e0e48d7dfdb9141caa + EXAMPLE_SUITE_REVISION: 599987b52bd2d064e26299d3317b11049499f32b cache: paths: - .cache/pip @@ -502,19 +517,27 @@ Ubuntu_GCC_integration_tests_asan: .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/ script: - - CI/before_install.osx.sh + - CI/before_install.macos.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" - - ccache -z -M "${CCACHE_SIZE}" - - CI/before_script.osx.sh - - cd build; make -j $(sysctl -n hw.logicalcpu) package - - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}.dmg"; done + - CI/macos/ccache_prep.sh + - CI/before_script.macos.sh + - CI/macos/build.sh + - cd build + - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${DMG_IDENTIFIER}_${CI_COMMIT_REF_NAME##*/}.dmg"; done - | if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then echo "[default]" > ~/.s3cfg @@ -529,20 +552,33 @@ Ubuntu_GCC_integration_tests_asan: s3cmd put "${dmg}" s3://openmw-artifacts/${artifactDirectory} done fi - - ccache -s + - ../CI/macos/ccache_save.sh artifacts: paths: - build/OpenMW-*.dmg -macOS14_Xcode15_arm64: +macOS15_Xcode16_amd64: extends: .MacOS - image: macos-14-xcode-15 - tags: - - saas-macos-medium-m1 cache: - key: macOS14_Xcode15_arm64.v1 + 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 + +macOS15_Xcode16_arm64: + extends: .MacOS + cache: + 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 @@ -667,7 +703,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 @@ -825,7 +861,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 @@ -926,7 +962,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 +1004,7 @@ Windows_MSBuild_CacheInit: - flatpak build-bundle ./repo openmw.flatpak org.openmw.OpenMW.devel cache: key: flatpak - paths: + paths: - ".flatpak-builder" artifacts: untracked: false diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 962b34f516..926dd3dbf6 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,4 +10,4 @@ python: build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.9" diff --git a/AUTHORS.md b/AUTHORS.md index d8ea649782..6f30324217 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,10 +50,10 @@ Programmers Berulacks Bo Svensson Britt Mathis (galdor557) - Capostrophic Carl Maxwell cc9cii Cédric Mocquillon + Charles Horn Chris Boyce (slothlife) Chris Robinson (KittyCat) Chris Vigil @@ -196,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 fa0c43be25..5435835606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +0.50.0 +------ + + Bug #2967: Inventory windows don't update when changing items by script + 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 #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 #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 #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 #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 #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 #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 #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 #8615: Rest/wait time progress speed is different from vanilla + Feature #2522: Support quick item transfer + Feature #3769: Allow GetSpellEffects on enchantments + 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 #8580: Sort characters in the save loading menu + Feature #8597: Lua: Add more built-in event handlers + 0.49.0 ------ @@ -236,6 +298,8 @@ Bug #8465: Blue screen w/ antialiasing and post-processing on macOS Bug #8503: Camera does not handle NaN gracefully Bug #8541: Lua: util.color:asHex produces wrong output for some colors + Bug #8567: Token replacement does not work via CLI and relative paths passed via the command line are not relative to the CWD + Bug #8576: Crash on exit when unresolving containers with scripted items Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking diff --git a/CI/before_install.macos.sh b/CI/before_install.macos.sh new file mode 100755 index 0000000000..60d2d3a38c --- /dev/null +++ b/CI/before_install.macos.sh @@ -0,0 +1,7 @@ +#!/bin/sh -ex + +if [[ "${MACOS_AMD64}" ]]; then + ./CI/macos/before_install.amd64.sh +else + ./CI/macos/before_install.arm64.sh +fi diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh deleted file mode 100755 index d73399c102..0000000000 --- a/CI/before_install.osx.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -ex - -export HOMEBREW_NO_EMOJI=1 -export HOMEBREW_NO_INSTALL_CLEANUP=1 -export HOMEBREW_AUTOREMOVE=1 - -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@5 -export PATH="/opt/homebrew/opt/qt@5/bin:$PATH" - -# Install deps -brew install openal-soft icu4c yaml-cpp sqlite - -ccache --version -cmake --version -qmake --version - -if [[ "${MACOS_AMD64}" ]]; then - curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip - unzip -o ~/openmw-deps.zip -d /tmp > /dev/null -else - 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 -fi diff --git a/CI/before_script.macos.sh b/CI/before_script.macos.sh new file mode 100755 index 0000000000..50adab234d --- /dev/null +++ b/CI/before_script.macos.sh @@ -0,0 +1,77 @@ +#!/bin/sh -e + +# Silence a git warning +git config --global advice.detachedHead false + +rm -fr build +mkdir build +cd build + +DEPENDENCIES_ROOT="/tmp/openmw-deps" + +if [[ "${MACOS_AMD64}" ]]; then + QT_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix qt@6) + ICU_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix icu4c) + OPENAL_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix openal-soft) + CCACHE_EXECUTABLE=$(arch -x86_64 /usr/local/bin/brew --prefix ccache)/bin/ccache +else + QT_PATH=$(brew --prefix qt@6) + ICU_PATH=$(brew --prefix icu4c) + OPENAL_PATH=$(brew --prefix openal-soft) + CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache +fi + +declare -a CMAKE_CONF_OPTS=( +-D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH" +-D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" +-D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" +-D CMAKE_CXX_FLAGS="-stdlib=libc++" +-D CMAKE_C_COMPILER="clang" +-D CMAKE_CXX_COMPILER="clang++" +-D CMAKE_OSX_DEPLOYMENT_TARGET="13.6" +-D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE +-D Boost_INCLUDE_DIR="$DEPENDENCIES_ROOT/include" +-D OSGPlugins_LIB_DIR="$DEPENDENCIES_ROOT/lib/osgPlugins-3.6.5" +-D ICU_ROOT="$ICU_PATH" +-D OPENMW_OSX_DEPLOYMENT=TRUE +) + +declare -a BUILD_OPTS=( +-D BUILD_OPENMW=TRUE +-D BUILD_OPENCS=TRUE +-D BUILD_ESMTOOL=TRUE +-D BUILD_BSATOOL=TRUE +-D BUILD_ESSIMPORTER=TRUE +-D BUILD_NIFTEST=TRUE +-D BUILD_NAVMESHTOOL=TRUE +-D BUILD_BULLETOBJECTTOOL=TRUE +-G"Unix Makefiles" +) + +if [[ "${MACOS_AMD64}" ]]; then + CMAKE_CONF_OPTS+=( + -D CMAKE_OSX_ARCHITECTURES="x86_64" + ) +fi + +if [[ "${CMAKE_BUILD_TYPE}" ]]; then + CMAKE_CONF_OPTS+=( + -D CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ) +else + CMAKE_CONF_OPTS+=( + -D CMAKE_BUILD_TYPE=RelWithDebInfo + ) +fi + +if [[ "${MACOS_AMD64}" ]]; then + arch -x86_64 cmake \ + "${CMAKE_CONF_OPTS[@]}" \ + "${BUILD_OPTS[@]}" \ + .. +else + cmake \ + "${CMAKE_CONF_OPTS[@]}" \ + "${BUILD_OPTS[@]}" \ + .. +fi diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 940c72c806..9363211e9a 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 diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh deleted file mode 100755 index 9f7a5bde8f..0000000000 --- a/CI/before_script.osx.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh -e - -# Silence a git warning -git config --global advice.detachedHead false - -rm -fr build -mkdir build -cd build - -DEPENDENCIES_ROOT="/tmp/openmw-deps" - -QT_PATH=$(brew --prefix qt@5) -ICU_PATH=$(brew --prefix icu4c) -OPENAL_PATH=$(brew --prefix openal-soft) -CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache - -declare -a CMAKE_CONF_OPTS=( --D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH" --D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" --D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" --D CMAKE_CXX_FLAGS="-stdlib=libc++" --D CMAKE_C_COMPILER="clang" --D CMAKE_CXX_COMPILER="clang++" --D CMAKE_OSX_DEPLOYMENT_TARGET="13.6" --D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE --D Boost_INCLUDE_DIR="$DEPENDENCIES_ROOT/include" --D OSGPlugins_LIB_DIR="$DEPENDENCIES_ROOT/lib/osgPlugins-3.6.5" --D ICU_ROOT="$ICU_PATH" --D OPENMW_OSX_DEPLOYMENT=TRUE -) - -if [[ "${CMAKE_BUILD_TYPE}" ]]; then - CMAKE_CONF_OPTS+=( - -D CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - ) -else - CMAKE_CONF_OPTS+=( - -D CMAKE_BUILD_TYPE=RelWithDebInfo - ) -fi - -cmake \ -"${CMAKE_CONF_OPTS[@]}" \ --D BUILD_OPENMW=TRUE \ --D BUILD_OPENCS=TRUE \ --D BUILD_ESMTOOL=TRUE \ --D BUILD_BSATOOL=TRUE \ --D BUILD_ESSIMPORTER=TRUE \ --D BUILD_NIFTEST=TRUE \ --D BUILD_NAVMESHTOOL=TRUE \ --D BUILD_BULLETOBJECTTOOL=TRUE \ --G"Unix Makefiles" \ -.. diff --git a/CI/check_file_names.sh b/CI/check_file_names.sh index b04416659e..2a0f421cc9 100755 --- a/CI/check_file_names.sh +++ b/CI/check_file_names.sh @@ -1,7 +1,6 @@ #!/bin/bash -ex git ls-files -- ':(exclude)extern/' '*.cpp' '*.hpp' '*.h' | - grep -vP '/[a-z0-9]+\.(cpp|hpp|h)$' | - grep -vFf CI/file_name_exceptions.txt && + grep -vP '/[a-z0-9]+\.(cpp|hpp|h)$' && ( echo 'File names do not follow the naming convention, see https://wiki.openmw.org/index.php?title=Naming_Conventions#Files'; exit -1 ) exit 0 diff --git a/CI/file_name_exceptions.txt b/CI/file_name_exceptions.txt deleted file mode 100644 index 14d106169b..0000000000 --- a/CI/file_name_exceptions.txt +++ /dev/null @@ -1,48 +0,0 @@ -apps/openmw/android_main.cpp -apps/openmw/mwsound/efx-presets.h -apps/openmw/mwsound/ffmpeg_decoder.cpp -apps/openmw/mwsound/ffmpeg_decoder.hpp -apps/openmw/mwsound/openal_output.cpp -apps/openmw/mwsound/openal_output.hpp -apps/openmw/mwsound/sound_buffer.cpp -apps/openmw/mwsound/sound_buffer.hpp -apps/openmw/mwsound/sound_decoder.hpp -apps/openmw/mwsound/sound_output.hpp -apps/components_tests/esm/test_fixed_string.cpp -apps/components_tests/files/conversion_tests.cpp -apps/components_tests/lua/test_async.cpp -apps/components_tests/lua/test_configuration.cpp -apps/components_tests/lua/test_l10n.cpp -apps/components_tests/lua/test_lua.cpp -apps/components_tests/lua/test_scriptscontainer.cpp -apps/components_tests/lua/test_serialization.cpp -apps/components_tests/lua/test_storage.cpp -apps/components_tests/lua/test_ui_content.cpp -apps/components_tests/lua/test_utilpackage.cpp -apps/components_tests/lua/test_inputactions.cpp -apps/components_tests/lua/test_yaml.cpp -apps/components_tests/misc/test_endianness.cpp -apps/components_tests/misc/test_resourcehelpers.cpp -apps/components_tests/misc/test_stringops.cpp -apps/openmw_tests/mwdialogue/test_keywordsearch.cpp -apps/openmw_tests/mwscript/test_scripts.cpp -apps/openmw_tests/mwscript/test_utils.hpp -apps/openmw_tests/mwworld/test_store.cpp -components/bsa/bsa_file.cpp -components/bsa/bsa_file.hpp -components/crashcatcher/windows_crashcatcher.cpp -components/crashcatcher/windows_crashcatcher.hpp -components/crashcatcher/windows_crashmonitor.cpp -components/crashcatcher/windows_crashmonitor.hpp -components/crashcatcher/windows_crashshm.hpp -components/fx/lexer_types.hpp -components/fx/parse_constants.hpp -components/platform/file.posix.cpp -components/platform/file.stdio.cpp -components/platform/file.win32.cpp -components/sdlutil/gl4es_init.cpp -components/sdlutil/gl4es_init.h -components/to_utf8/gen_iconv.cpp -components/to_utf8/tables_gen.hpp -components/to_utf8/to_utf8.cpp -components/to_utf8/to_utf8.hpp diff --git a/CI/macos/before_install.amd64.sh b/CI/macos/before_install.amd64.sh new file mode 100755 index 0000000000..642f2c2c62 --- /dev/null +++ b/CI/macos/before_install.amd64.sh @@ -0,0 +1,8 @@ +#!/bin/sh -ex + +arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +arch -x86_64 /usr/local/bin/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-20240802.zip -o ~/openmw-deps.zip +unzip -o ~/openmw-deps.zip -d /tmp > /dev/null diff --git a/CI/macos/before_install.arm64.sh b/CI/macos/before_install.arm64.sh new file mode 100755 index 0000000000..d53d847b1c --- /dev/null +++ b/CI/macos/before_install.arm64.sh @@ -0,0 +1,9 @@ +#!/bin/sh -ex + +brew tap --repair +brew update --quiet + +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/build.sh b/CI/macos/build.sh new file mode 100755 index 0000000000..64986717ce --- /dev/null +++ b/CI/macos/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh -ex + +cd build + +if [[ "${MACOS_AMD64}" ]]; then + arch -x86_64 make -j $(sysctl -n hw.logicalcpu) package +else + make -j $(sysctl -n hw.logicalcpu) package +fi diff --git a/CI/macos/ccache_prep.sh b/CI/macos/ccache_prep.sh new file mode 100755 index 0000000000..abd0103be0 --- /dev/null +++ b/CI/macos/ccache_prep.sh @@ -0,0 +1,7 @@ +#!/bin/sh -ex + +if [[ "${MACOS_AMD64}" ]]; then + arch -x86_64 ccache -z -M "${CCACHE_SIZE}" +else + ccache -z -M "${CCACHE_SIZE}" +fi diff --git a/CI/macos/ccache_save.sh b/CI/macos/ccache_save.sh new file mode 100755 index 0000000000..d7cc48c25e --- /dev/null +++ b/CI/macos/ccache_save.sh @@ -0,0 +1,7 @@ +#!/bin/sh -ex + +if [[ "${MACOS_AMD64}" ]]; then + arch -x86_64 ccache -svv +else + ccache -svv +fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dd4f0ac27..6103ddcb33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,10 +80,10 @@ endif() message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) -set(OPENMW_VERSION_MINOR 49) +set(OPENMW_VERSION_MINOR 50) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 76) -set(OPENMW_POSTPROCESSING_API_REVISION 2) +set(OPENMW_LUA_API_REVISION 82) +set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") @@ -466,7 +466,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 +590,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 +604,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 +729,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/README.md b/README.md index bca5851c7b..f65f74e9d8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ OpenMW is an open-source open-world RPG game engine that supports playing Morrow OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. -* Version: 0.49.0 +* Version: 0.50.0 * License: GPLv3 (see [LICENSE](https://gitlab.com/OpenMW/openmw/-/raw/master/LICENSE) for more information) * Website: https://www.openmw.org * IRC: #openmw on irc.libera.chat diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp index 5cb275a53d..659d97b7f1 100644 --- a/apps/bulletobjecttool/main.cpp +++ b/apps/bulletobjecttool/main.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include #include #include #include @@ -125,6 +125,7 @@ namespace } Files::ConfigurationManager config; + config.processPaths(variables, std::filesystem::current_path()); config.readConfiguration(variables, desc); Debug::setupLogging(config.getLogPath(), applicationName); @@ -154,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/CMakeLists.txt b/apps/components_tests/CMakeLists.txt index 22bb542538..7595681313 100644 --- a/apps/components_tests/CMakeLists.txt +++ b/apps/components_tests/CMakeLists.txt @@ -4,29 +4,28 @@ include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS}) file(GLOB UNITTEST_SRC_FILES main.cpp - esm/test_fixed_string.cpp - esm/variant.cpp + esm/testfixedstring.cpp esm/testrefid.cpp + esm/variant.cpp - lua/test_lua.cpp - lua/test_scriptscontainer.cpp - lua/test_utilpackage.cpp - lua/test_serialization.cpp - lua/test_configuration.cpp - lua/test_l10n.cpp - lua/test_storage.cpp - lua/test_async.cpp - lua/test_inputactions.cpp - lua/test_yaml.cpp - - lua/test_ui_content.cpp + lua/testasync.cpp + lua/testconfiguration.cpp + lua/testinputactions.cpp + lua/testl10n.cpp + lua/testlua.cpp + lua/testscriptscontainer.cpp + lua/testserialization.cpp + lua/teststorage.cpp + lua/testuicontent.cpp + lua/testutilpackage.cpp + lua/testyaml.cpp misc/compression.cpp misc/progressreporter.cpp - misc/test_endianness.cpp - misc/test_resourcehelpers.cpp - misc/test_stringops.cpp + misc/testendianness.cpp misc/testmathutil.cpp + misc/testresourcehelpers.cpp + misc/teststringops.cpp nifloader/testbulletnifloader.cpp @@ -64,8 +63,8 @@ file(GLOB UNITTEST_SRC_FILES esmloader/esmdata.cpp esmloader/record.cpp + files/conversiontests.cpp files/hash.cpp - files/conversion_tests.cpp toutf8/toutf8.cpp diff --git a/apps/components_tests/esm/test_fixed_string.cpp b/apps/components_tests/esm/testfixedstring.cpp similarity index 100% rename from apps/components_tests/esm/test_fixed_string.cpp rename to apps/components_tests/esm/testfixedstring.cpp 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/esmloader/load.cpp b/apps/components_tests/esmloader/load.cpp index 20e06507d1..e5e1b15876 100644 --- a/apps/components_tests/esmloader/load.cpp +++ b/apps/components_tests/esmloader/load.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include diff --git a/apps/components_tests/files/conversion_tests.cpp b/apps/components_tests/files/conversiontests.cpp similarity index 100% rename from apps/components_tests/files/conversion_tests.cpp rename to apps/components_tests/files/conversiontests.cpp 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/test_async.cpp b/apps/components_tests/lua/testasync.cpp similarity index 100% rename from apps/components_tests/lua/test_async.cpp rename to apps/components_tests/lua/testasync.cpp diff --git a/apps/components_tests/lua/test_configuration.cpp b/apps/components_tests/lua/testconfiguration.cpp similarity index 100% rename from apps/components_tests/lua/test_configuration.cpp rename to apps/components_tests/lua/testconfiguration.cpp diff --git a/apps/components_tests/lua/test_inputactions.cpp b/apps/components_tests/lua/testinputactions.cpp similarity index 100% rename from apps/components_tests/lua/test_inputactions.cpp rename to apps/components_tests/lua/testinputactions.cpp diff --git a/apps/components_tests/lua/test_l10n.cpp b/apps/components_tests/lua/testl10n.cpp similarity index 85% rename from apps/components_tests/lua/test_l10n.cpp rename to apps/components_tests/lua/testl10n.cpp index b48028a730..7446822f7f 100644 --- a/apps/components_tests/lua/test_l10n.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/test_lua.cpp b/apps/components_tests/lua/testlua.cpp similarity index 100% rename from apps/components_tests/lua/test_lua.cpp rename to apps/components_tests/lua/testlua.cpp diff --git a/apps/components_tests/lua/test_scriptscontainer.cpp b/apps/components_tests/lua/testscriptscontainer.cpp similarity index 100% rename from apps/components_tests/lua/test_scriptscontainer.cpp rename to apps/components_tests/lua/testscriptscontainer.cpp diff --git a/apps/components_tests/lua/test_serialization.cpp b/apps/components_tests/lua/testserialization.cpp similarity index 100% rename from apps/components_tests/lua/test_serialization.cpp rename to apps/components_tests/lua/testserialization.cpp diff --git a/apps/components_tests/lua/test_storage.cpp b/apps/components_tests/lua/teststorage.cpp similarity index 100% rename from apps/components_tests/lua/test_storage.cpp rename to apps/components_tests/lua/teststorage.cpp diff --git a/apps/components_tests/lua/test_ui_content.cpp b/apps/components_tests/lua/testuicontent.cpp similarity index 100% rename from apps/components_tests/lua/test_ui_content.cpp rename to apps/components_tests/lua/testuicontent.cpp diff --git a/apps/components_tests/lua/test_utilpackage.cpp b/apps/components_tests/lua/testutilpackage.cpp similarity index 89% rename from apps/components_tests/lua/test_utilpackage.cpp rename to apps/components_tests/lua/testutilpackage.cpp index a61c0e0306..47041985f6 100644 --- a/apps/components_tests/lua/test_utilpackage.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/lua/test_yaml.cpp b/apps/components_tests/lua/testyaml.cpp similarity index 100% rename from apps/components_tests/lua/test_yaml.cpp rename to apps/components_tests/lua/testyaml.cpp diff --git a/apps/components_tests/misc/test_endianness.cpp b/apps/components_tests/misc/testendianness.cpp similarity index 100% rename from apps/components_tests/misc/test_endianness.cpp rename to apps/components_tests/misc/testendianness.cpp diff --git a/apps/components_tests/misc/test_resourcehelpers.cpp b/apps/components_tests/misc/testresourcehelpers.cpp similarity index 81% rename from apps/components_tests/misc/test_resourcehelpers.cpp rename to apps/components_tests/misc/testresourcehelpers.cpp index 05079ae875..b21ecb2e14 100644 --- a/apps/components_tests/misc/test_resourcehelpers.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/components_tests/misc/test_stringops.cpp b/apps/components_tests/misc/teststringops.cpp similarity index 100% rename from apps/components_tests/misc/test_stringops.cpp rename to apps/components_tests/misc/teststringops.cpp diff --git a/apps/components_tests/toutf8/toutf8.cpp b/apps/components_tests/toutf8/toutf8.cpp index 704ee6742d..3c84da65ee 100644 --- a/apps/components_tests/toutf8/toutf8.cpp +++ b/apps/components_tests/toutf8/toutf8.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include diff --git a/apps/esmtool/tes4.cpp b/apps/esmtool/tes4.cpp index 5b657da573..4ff632a217 100644 --- a/apps/esmtool/tes4.cpp +++ b/apps/esmtool/tes4.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include namespace EsmTool { 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/essimporter/importer.cpp b/apps/essimporter/importer.cpp index 5cc9a8259b..cf04fee163 100644 --- a/apps/essimporter/importer.cpp +++ b/apps/essimporter/importer.cpp @@ -25,7 +25,7 @@ #include -#include +#include #include "importercontext.hpp" diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp index f0833e9d81..50e11e2c5a 100644 --- a/apps/essimporter/main.cpp +++ b/apps/essimporter/main.cpp @@ -40,6 +40,7 @@ Allowed options)"); bpo::notify(variables); Files::ConfigurationManager cfgManager(true); + cfgManager.processPaths(variables, std::filesystem::current_path()); cfgManager.readConfiguration(variables, desc); const auto& essFile = variables["mwsave"].as(); diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index a4fb7fb0d9..6986b69030 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -39,8 +39,6 @@ #include "utils/profilescombobox.hpp" #include "utils/textinputdialog.hpp" -#include "ui_directorypicker.h" - const char* Launcher::DataFilesPage::mDefaultContentListName = "Default"; namespace @@ -155,6 +153,7 @@ namespace Launcher Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, Config::LauncherSettings& launcherSettings, MainDialog* parent) : QWidget(parent) + , mDirectoryPickerDialog(new QDialog(this)) , mMainDialog(parent) , mCfgMgr(cfg) , mGameSettings(gameSettings) @@ -163,6 +162,7 @@ Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, C , mReloadCellsThread(&DataFilesPage::reloadCells, this) { ui.setupUi(this); + mDirectoryPicker.setupUi(mDirectoryPickerDialog); setObjectName("DataFilesPage"); mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true); const QString encoding = mGameSettings.value("encoding", { "win1252" }).value; @@ -266,6 +266,7 @@ void Launcher::DataFilesPage::buildView() buildArchiveContextMenu(); buildDataFilesContextMenu(); + buildDirectoryPickerContextMenu(); } void Launcher::DataFilesPage::slotCopySelectedItemsPaths() @@ -298,8 +299,10 @@ void Launcher::DataFilesPage::buildArchiveContextMenu() &DataFilesPage::slotShowArchiveContextMenu); mArchiveContextMenu = new QMenu(ui.archiveListWidget); - mArchiveContextMenu->addAction(tr("&Check Selected"), this, SLOT(slotCheckMultiSelectedItems())); - mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this, SLOT(slotUncheckMultiSelectedItems())); + mArchiveContextMenu->addAction(tr("&Check Selected"), this, + [this]() { setCheckStateForMultiSelectedItems(ui.archiveListWidget, Qt::Checked); }); + mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this, + [this]() { setCheckStateForMultiSelectedItems(ui.archiveListWidget, Qt::Unchecked); }); } void Launcher::DataFilesPage::buildDataFilesContextMenu() @@ -314,6 +317,18 @@ void Launcher::DataFilesPage::buildDataFilesContextMenu() tr("&Open Path in File Explorer"), this, &Launcher::DataFilesPage::slotOpenSelectedItemsPaths); } +void Launcher::DataFilesPage::buildDirectoryPickerContextMenu() +{ + connect(mDirectoryPicker.dirListWidget, &QListWidget::customContextMenuRequested, this, + &DataFilesPage::slotShowDirectoryPickerContextMenu); + + mDirectoryPickerMenu = new QMenu(mDirectoryPicker.dirListWidget); + mDirectoryPickerMenu->addAction(tr("&Check Selected"), this, + [this]() { setCheckStateForMultiSelectedItems(mDirectoryPicker.dirListWidget, Qt::Checked); }); + mDirectoryPickerMenu->addAction(tr("&Uncheck Selected"), this, + [this]() { setCheckStateForMultiSelectedItems(mDirectoryPicker.dirListWidget, Qt::Unchecked); }); +} + bool Launcher::DataFilesPage::loadSettings() { ui.navMeshMaxSizeSpinBox->setValue(getMaxNavMeshDbFileSizeMiB()); @@ -821,28 +836,22 @@ void Launcher::DataFilesPage::addSubdirectories(bool append) return; } - QDialog dialog; - Ui::SelectSubdirs select; - - select.setupUi(&dialog); + mDirectoryPicker.dirListWidget->clear(); for (const auto& dir : subdirs) { if (!ui.directoryListWidget->findItems(dir, Qt::MatchFixedString).isEmpty()) continue; - const auto lastRow = select.dirListWidget->count(); - select.dirListWidget->addItem(dir); - select.dirListWidget->item(lastRow)->setCheckState(Qt::Unchecked); + QListWidgetItem* newDir = new QListWidgetItem(dir, mDirectoryPicker.dirListWidget); + newDir->setCheckState(Qt::Unchecked); } - dialog.show(); - - if (dialog.exec() == QDialog::Rejected) + if (mDirectoryPickerDialog->exec() == QDialog::Rejected) return; - for (int i = 0; i < select.dirListWidget->count(); ++i) + for (int i = 0; i < mDirectoryPicker.dirListWidget->count(); ++i) { - const auto* dir = select.dirListWidget->item(i); + const auto* dir = mDirectoryPicker.dirListWidget->item(i); if (dir->checkState() == Qt::Checked) { ui.directoryListWidget->insertItem(selectedRow, dir->text()); @@ -893,38 +902,35 @@ void Launcher::DataFilesPage::removeDirectory() refreshDataFilesView(); } +void Launcher::DataFilesPage::showContextMenu(QMenu* menu, QListWidget* list, const QPoint& pos) +{ + QPoint globalPos = list->viewport()->mapToGlobal(pos); + menu->exec(globalPos); +} + void Launcher::DataFilesPage::slotShowArchiveContextMenu(const QPoint& pos) { - QPoint globalPos = ui.archiveListWidget->viewport()->mapToGlobal(pos); - mArchiveContextMenu->exec(globalPos); + showContextMenu(mArchiveContextMenu, ui.archiveListWidget, pos); } void Launcher::DataFilesPage::slotShowDataFilesContextMenu(const QPoint& pos) { - QPoint globalPos = ui.directoryListWidget->viewport()->mapToGlobal(pos); - mDataFilesContextMenu->exec(globalPos); + showContextMenu(mDataFilesContextMenu, ui.directoryListWidget, pos); } -void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(bool checked) +void Launcher::DataFilesPage::slotShowDirectoryPickerContextMenu(const QPoint& pos) { - Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked; + showContextMenu(mDirectoryPickerMenu, mDirectoryPicker.dirListWidget, pos); +} - for (QListWidgetItem* selectedItem : ui.archiveListWidget->selectedItems()) +void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(QListWidget* list, Qt::CheckState checkState) +{ + for (QListWidgetItem* selectedItem : list->selectedItems()) { selectedItem->setCheckState(checkState); } } -void Launcher::DataFilesPage::slotUncheckMultiSelectedItems() -{ - setCheckStateForMultiSelectedItems(false); -} - -void Launcher::DataFilesPage::slotCheckMultiSelectedItems() -{ - setCheckStateForMultiSelectedItems(true); -} - void Launcher::DataFilesPage::moveSources(QListWidget* sourceList, int step) { const QList> sortedItems = sortedSelectedItems(sourceList, step > 0); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index ca40bd1319..847cfe5a18 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -2,6 +2,7 @@ #define DATAFILESPAGE_H #include "ui_datafilespage.h" +#include "ui_directorypicker.h" #include @@ -46,8 +47,11 @@ namespace Launcher ContentSelectorView::ContentSelector* mSelector; Ui::DataFilesPage ui; + QDialog* mDirectoryPickerDialog; + Ui::SelectSubdirs mDirectoryPicker; QMenu* mArchiveContextMenu; QMenu* mDataFilesContextMenu; + QMenu* mDirectoryPickerMenu; public: explicit DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, @@ -87,8 +91,7 @@ namespace Launcher void slotShowArchiveContextMenu(const QPoint& pos); void slotShowDataFilesContextMenu(const QPoint& pos); - void slotCheckMultiSelectedItems(); - void slotUncheckMultiSelectedItems(); + void slotShowDirectoryPickerContextMenu(const QPoint& pos); void on_newProfileAction_triggered(); void on_cloneProfileAction_triggered(); @@ -145,7 +148,9 @@ namespace Launcher void buildView(); void buildArchiveContextMenu(); void buildDataFilesContextMenu(); - void setCheckStateForMultiSelectedItems(bool checked); + void buildDirectoryPickerContextMenu(); + void showContextMenu(QMenu* menu, QListWidget* list, const QPoint& pos); + void setCheckStateForMultiSelectedItems(QListWidget* list, Qt::CheckState checkState); void setProfile(int index, bool savePrevious); void setProfile(const QString& previous, const QString& current, bool savePrevious); void removeProfile(const QString& profile); 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/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/ui/directorypicker.ui b/apps/launcher/ui/directorypicker.ui index 6350bcd2d3..c32a78b53e 100644 --- a/apps/launcher/ui/directorypicker.ui +++ b/apps/launcher/ui/directorypicker.ui @@ -25,7 +25,14 @@ - + + + QAbstractItemView::ExtendedSelection + + + Qt::CustomContextMenu + + diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 8c7c238b4a..a8dee709da 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -11,6 +11,23 @@ namespace sfs = std::filesystem; +namespace +{ + // from configfileparser.cpp + std::string trim_ws(const std::string& s) + { + std::string::size_type n, n2; + n = s.find_first_not_of(" \t\r\n"); + if (n == std::string::npos) + return std::string(); + else + { + n2 = s.find_last_not_of(" \t\r\n"); + return s.substr(n, n2 - n + 1); + } + } +} + MwIniImporter::MwIniImporter() : mVerbose(false) , mEncoding(ToUTF8::WINDOWS_1250) @@ -352,12 +369,10 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::filesystem::pat std::string line; while (std::getline(file, line)) { - - // we cant say comment by only looking at first char anymore - int comment_pos = static_cast(line.find('#')); - if (comment_pos > 0) + // ignore comments - keep in sync with configfileparser.cpp + if (line.find('#') == line.find_first_not_of(" \t\r\n")) { - line = line.substr(0, comment_pos); + continue; } if (line.empty()) @@ -373,6 +388,8 @@ MwIniImporter::multistrmap MwIniImporter::loadCfgFile(const std::filesystem::pat std::string key(line.substr(0, pos)); std::string value(line.substr(pos + 1)); + key = trim_ws(key); + value = trim_ws(value); if (map.find(key) == map.end()) { diff --git a/apps/mwiniimporter/importer.hpp b/apps/mwiniimporter/importer.hpp index 4c42caf5a3..342670713c 100644 --- a/apps/mwiniimporter/importer.hpp +++ b/apps/mwiniimporter/importer.hpp @@ -8,7 +8,7 @@ #include #include -#include +#include class MwIniImporter { diff --git a/apps/mwiniimporter/main.cpp b/apps/mwiniimporter/main.cpp index 0beb2b38cc..6e4242cb4e 100644 --- a/apps/mwiniimporter/main.cpp +++ b/apps/mwiniimporter/main.cpp @@ -126,12 +126,20 @@ int wmain(int argc, wchar_t* wargv[]) MwIniImporter importer; importer.setVerbose(vm.count("verbose") != 0); + MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); + // Font encoding settings - std::string encoding(vm["encoding"].as()); + std::string encoding; + if (vm["encoding"].defaulted() && cfg.contains("encoding") && !cfg["encoding"].empty()) + encoding = cfg["encoding"].back(); + else + { + encoding = vm["encoding"].as(); + cfg["encoding"] = { encoding }; + } importer.setInputEncoding(ToUTF8::calculateEncoding(encoding)); MwIniImporter::multistrmap ini = importer.loadIniFile(iniFile); - MwIniImporter::multistrmap cfg = importer.loadCfgFile(cfgFile); if (!vm.count("fonts")) { diff --git a/apps/navmeshtool/main.cpp b/apps/navmeshtool/main.cpp index e148e60d54..6cfa7fc61d 100644 --- a/apps/navmeshtool/main.cpp +++ b/apps/navmeshtool/main.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include #include @@ -143,6 +143,7 @@ namespace NavMeshTool } Files::ConfigurationManager config; + config.processPaths(variables, std::filesystem::current_path()); config.readConfiguration(variables, desc); Debug::setupLogging(config.getLogPath(), applicationName); @@ -188,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..0a278316e3 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 diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 8781f54154..ab361e7568 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -34,7 +34,7 @@ #include #include #include -#include +#include #include "view/doc/viewmanager.hpp" diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index 60516cdc8c..ac8f06712c 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include "../world/data.hpp" #include "../world/idcompletionmanager.hpp" diff --git a/apps/opencs/model/doc/documentmanager.hpp b/apps/opencs/model/doc/documentmanager.hpp index 2c9ee1e98e..53caf04a0a 100644 --- a/apps/opencs/model/doc/documentmanager.hpp +++ b/apps/opencs/model/doc/documentmanager.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include "loader.hpp" diff --git a/apps/opencs/model/doc/runner.cpp b/apps/opencs/model/doc/runner.cpp index d647d6b498..4019fbfe57 100644 --- a/apps/opencs/model/doc/runner.cpp +++ b/apps/opencs/model/doc/runner.cpp @@ -2,11 +2,8 @@ #include -#if defined(Q_OS_MAC) #include #include -#endif - #include #include #include @@ -55,16 +52,17 @@ void CSMDoc::Runner::start(bool delayed) QString path = "openmw"; #ifdef Q_OS_WIN - path.append(QString(".exe")); -#elif defined(Q_OS_MAC) - QDir dir(QCoreApplication::applicationDirPath()); - dir.cdUp(); - dir.cdUp(); - dir.cdUp(); - path = dir.absoluteFilePath(path.prepend("OpenMW.app/Contents/MacOS/")); -#else - path.prepend(QString("./")); + path.append(QLatin1String(".exe")); #endif + QDir dir(QCoreApplication::applicationDirPath()); +#ifdef Q_OS_MAC + // the CS and engine are in separate .app directories + dir.cdUp(); + dir.cdUp(); + dir.cdUp(); + path.prepend("OpenMW.app/Contents/MacOS/"); +#endif + path = dir.absoluteFilePath(path); mStartup = new QTemporaryFile(this); mStartup->open(); diff --git a/apps/opencs/model/doc/saving.hpp b/apps/opencs/model/doc/saving.hpp index 5dcdbb6803..6197798fdf 100644 --- a/apps/opencs/model/doc/saving.hpp +++ b/apps/opencs/model/doc/saving.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "operation.hpp" #include "savingstate.hpp" diff --git a/apps/opencs/model/doc/savingstate.hpp b/apps/opencs/model/doc/savingstate.hpp index c42dff0366..af8caa2c4d 100644 --- a/apps/opencs/model/doc/savingstate.hpp +++ b/apps/opencs/model/doc/savingstate.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include namespace CSMDoc { diff --git a/apps/opencs/model/prefs/shortcutmanager.cpp b/apps/opencs/model/prefs/shortcutmanager.cpp index d6686d31d9..ac032efffb 100644 --- a/apps/opencs/model/prefs/shortcutmanager.cpp +++ b/apps/opencs/model/prefs/shortcutmanager.cpp @@ -78,7 +78,7 @@ namespace CSMPrefs } } - bool ShortcutManager::getModifier(const std::string& name, int& modifier) const + bool ShortcutManager::getModifier(std::string_view name, int& modifier) const { ModifierMap::const_iterator item = mModifiers.find(name); if (item != mModifiers.end()) @@ -175,14 +175,14 @@ namespace CSMPrefs return concat; } - void ShortcutManager::convertFromString(const std::string& data, QKeySequence& sequence) const + void ShortcutManager::convertFromString(std::string_view data, QKeySequence& sequence) const { const int MaxKeys = 4; // A limitation of QKeySequence size_t end = data.find(';'); size_t size = std::min(end, data.size()); - std::string value = data.substr(0, size); + std::string_view value = data.substr(0, size); size_t start = 0; int keyPos = 0; @@ -195,7 +195,7 @@ namespace CSMPrefs end = data.find('+', start); end = std::min(end, value.size()); - std::string name = value.substr(start, end - start); + std::string_view name = value.substr(start, end - start); if (name == "Ctrl") { @@ -242,12 +242,12 @@ namespace CSMPrefs sequence = QKeySequence(keys[0], keys[1], keys[2], keys[3]); } - void ShortcutManager::convertFromString(const std::string& data, int& modifier) const + void ShortcutManager::convertFromString(std::string_view data, int& modifier) const { size_t start = data.find(';') + 1; start = std::min(start, data.size()); - std::string name = data.substr(start); + std::string_view name = data.substr(start); KeyMap::const_iterator searchResult = mKeys.find(name); if (searchResult != mKeys.end()) { @@ -259,7 +259,7 @@ namespace CSMPrefs } } - void ShortcutManager::convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const + void ShortcutManager::convertFromString(std::string_view data, QKeySequence& sequence, int& modifier) const { convertFromString(data, sequence); convertFromString(data, modifier); diff --git a/apps/opencs/model/prefs/shortcutmanager.hpp b/apps/opencs/model/prefs/shortcutmanager.hpp index 0cfe3ad86a..fa25876660 100644 --- a/apps/opencs/model/prefs/shortcutmanager.hpp +++ b/apps/opencs/model/prefs/shortcutmanager.hpp @@ -31,7 +31,7 @@ namespace CSMPrefs bool getSequence(std::string_view name, QKeySequence& sequence) const; void setSequence(std::string_view name, const QKeySequence& sequence); - bool getModifier(const std::string& name, int& modifier) const; + bool getModifier(std::string_view name, int& modifier) const; void setModifier(std::string_view name, int modifier); std::string convertToString(const QKeySequence& sequence) const; @@ -39,10 +39,10 @@ namespace CSMPrefs std::string convertToString(const QKeySequence& sequence, int modifier) const; - void convertFromString(const std::string& data, QKeySequence& sequence) const; - void convertFromString(const std::string& data, int& modifier) const; + void convertFromString(std::string_view data, QKeySequence& sequence) const; + void convertFromString(std::string_view data, int& modifier) const; - void convertFromString(const std::string& data, QKeySequence& sequence, int& modifier) const; + void convertFromString(std::string_view data, QKeySequence& sequence, int& modifier) const; /// Replaces "{sequence-name}" or "{modifier-name}" with the appropriate text QString processToolTip(const QString& toolTip) const; @@ -53,7 +53,7 @@ namespace CSMPrefs typedef std::map> SequenceMap; typedef std::map> ModifierMap; typedef std::map NameMap; - typedef std::map KeyMap; + typedef std::map> KeyMap; ShortcutMap mShortcuts; SequenceMap mSequences; diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index f0af163bf2..b80d150448 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -180,7 +180,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 +379,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"); @@ -498,7 +502,7 @@ CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( // Setup with actual data QKeySequence sequence; - getShortcutManager().convertFromString(value, sequence); + getShortcutManager().convertFromString(value.get(), sequence); getShortcutManager().setSequence(value.mName, sequence); CSMPrefs::ShortcutSetting* setting diff --git a/apps/opencs/model/prefs/values.hpp b/apps/opencs/model/prefs/values.hpp index 1339fa62ed..16e5434d6b 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" }; diff --git a/apps/opencs/model/tools/mergeoperation.hpp b/apps/opencs/model/tools/mergeoperation.hpp index 2cce2bec0d..c50a7eefb0 100644 --- a/apps/opencs/model/tools/mergeoperation.hpp +++ b/apps/opencs/model/tools/mergeoperation.hpp @@ -3,7 +3,7 @@ #include -#include +#include #include "../doc/operation.hpp" diff --git a/apps/opencs/model/tools/mergestages.hpp b/apps/opencs/model/tools/mergestages.hpp index 42f06858b1..40cc799c81 100644 --- a/apps/opencs/model/tools/mergestages.hpp +++ b/apps/opencs/model/tools/mergestages.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include "../doc/stage.hpp" diff --git a/apps/opencs/model/tools/tools.hpp b/apps/opencs/model/tools/tools.hpp index c9e8937c90..939dea9bb8 100644 --- a/apps/opencs/model/tools/tools.hpp +++ b/apps/opencs/model/tools/tools.hpp @@ -6,7 +6,7 @@ #include -#include +#include #include 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/data.hpp b/apps/opencs/model/world/data.hpp index 237b746746..c119541381 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include "cell.hpp" #include "idcollection.hpp" 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/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 fb085f075a..76cfd57ba6 100644 --- a/apps/opencs/view/render/instancemode.cpp +++ b/apps/opencs/view/render/instancemode.cpp @@ -171,16 +171,18 @@ osg::Vec3f CSVRender::InstanceMode::getProjectionSpaceCoords(const osg::Vec3f& p osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart) { - osg::Matrix viewMatrix; - viewMatrix.invert(getWorldspaceWidget().getCamera()->getViewMatrix()); - osg::Matrix projMatrix; - projMatrix.invert(getWorldspaceWidget().getCamera()->getProjectionMatrix()); - osg::Matrix combined = projMatrix * viewMatrix; + const osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); + const osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); + const osg::Matrix combined = osg::Matrix::inverse(viewMatrix * projMatrix); /* calculate viewport normalized coordinates note: is there a reason to use getCamera()->getViewport()->computeWindowMatrix() instead? */ - float x = (point.x() * 2) / getWorldspaceWidget().getCamera()->getViewport()->width() - 1.0f; - float y = 1.0f - (point.y() * 2) / getWorldspaceWidget().getCamera()->getViewport()->height(); + const float scale = getWorldspaceWidget().devicePixelRatioF(); + const osg::Viewport* viewport = getWorldspaceWidget().getCamera()->getViewport(); + float x = point.x() * scale / viewport->width(); + float y = point.y() * scale / viewport->height(); + x = x * 2.0f - 1.0f; + y = 1.0f - y * 2.0f; osg::Vec3f mousePlanePoint = osg::Vec3f(x, y, dragStart.z()) * combined; @@ -360,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) @@ -458,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()); } } @@ -512,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; } @@ -589,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; } @@ -639,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(); 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..e84433f14c 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -445,6 +445,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..aeec5d191c 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; 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..915282ab45 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,6 +646,41 @@ 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); @@ -685,6 +734,8 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) } } + const QPointF& pos = event->localPos(); + 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..a223a54388 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 @@ -252,6 +265,10 @@ namespace CSVRender 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/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 37de0abeab..4dc2fd1fd1 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -15,7 +15,7 @@ set(OPENMW_HEADERS profile.hpp ) -source_group(apps/openmw FILES main.cpp android_main.cpp ${OPENMW_SOURCES} ${OPENMW_HEADERS} ${OPENMW_RESOURCES}) +source_group(apps/openmw FILES main.cpp androidmain.cpp ${OPENMW_SOURCES} ${OPENMW_HEADERS} ${OPENMW_RESOURCES}) add_openmw_dir (mwrender actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask @@ -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 @@ -60,9 +60,9 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant - context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings + context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings coremwscriptbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings dialoguebindings - postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker magicbindings factionbindings + postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker landbindings magicbindings factionbindings classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings 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 @@ -71,8 +71,8 @@ add_openmw_dir (mwlua ) add_openmw_dir (mwsound - soundmanagerimp openal_output ffmpeg_decoder sound sound_buffer sound_decoder sound_output - loudness movieaudiofactory alext efx efx-presets regionsoundselector watersoundupdater + soundmanagerimp openaloutput ffmpegdecoder sound soundbuffer sounddecoder soundoutput + loudness movieaudiofactory alext efx efxpresets regionsoundselector watersoundupdater ) add_openmw_dir (mwworld @@ -127,7 +127,7 @@ if(BUILD_OPENMW) if (ANDROID) add_library(openmw SHARED main.cpp - android_main.cpp + androidmain.cpp ) else() openmw_add_executable(openmw @@ -138,6 +138,12 @@ if(BUILD_OPENMW) endif() target_link_libraries(openmw openmw-lib) + + # Workaround necessary to ensure osgAnimation::MatrixLinearSampler dynamic casts work under Clang + # NOTE: it's unclear whether the broken behavior is spec-compliant + if (CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set_target_properties(openmw PROPERTIES ENABLE_EXPORTS ON) + endif() endif() # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING diff --git a/apps/openmw/android_main.cpp b/apps/openmw/androidmain.cpp similarity index 100% rename from apps/openmw/android_main.cpp rename to apps/openmw/androidmain.cpp diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 7dc372f269..244c458f46 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -404,8 +404,8 @@ OMW::Engine::~Engine() mMechanicsManager = nullptr; mDialogueManager = nullptr; mJournal = nullptr; - mScriptManager = nullptr; mWindowManager = nullptr; + mScriptManager = nullptr; mWorld = nullptr; mStereoManager = nullptr; mSoundManager = nullptr; @@ -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); @@ -1069,8 +1071,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/main.cpp b/apps/openmw/main.cpp index 812a6ffd85..12b327c222 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -61,6 +61,8 @@ bool parseOptions(int argc, char** argv, OMW::Engine& engine, Files::Configurati return false; } + cfgMgr.processPaths(variables, std::filesystem::current_path()); + cfgMgr.readConfiguration(variables, desc); Debug::setupLogging(cfgMgr.getLogPath(), "OpenMW"); 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..bbdb843199 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 @@ -75,6 +76,7 @@ namespace MWBase 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/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 5f96a4e095..532bc771ba 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -39,8 +39,8 @@ namespace MWSound class Sound; class Stream; - struct Sound_Decoder; - typedef std::shared_ptr DecoderPtr; + struct SoundDecoder; + typedef std::shared_ptr DecoderPtr; /* These must all fit together */ enum class PlayMode 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 20a27e5bc3..f268ed0e52 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -406,17 +406,16 @@ namespace MWBase virtual void enableActorCollision(const MWWorld::Ptr& actor, bool enable) = 0; - enum RestPermitted + enum RestFlags { - Rest_Allowed = 0, - Rest_OnlyWaiting = 1, + Rest_PlayerIsUnderwater = 1, Rest_PlayerIsInAir = 2, - Rest_PlayerIsUnderwater = 3, - Rest_EnemiesAreNearby = 4 + Rest_EnemiesAreNearby = 4, + Rest_CanSleep = 8, }; /// check if the player is allowed to rest - virtual RestPermitted canRest() const = 0; + virtual int canRest() const = 0; /// \todo Probably shouldn't be here virtual MWRender::Animation* getAnimation(const MWWorld::Ptr& ptr) = 0; 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..a1c632dab9 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -360,16 +360,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 @@ -534,10 +530,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 +544,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; @@ -871,7 +867,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/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..693caeb5ae 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -990,15 +990,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 +1006,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); @@ -1427,7 +1422,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 +1504,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/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 089c8c2894..3524446b26 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -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/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 295d690ce5..b7d4a1361c 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -265,10 +265,6 @@ bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& sele bool MWDialogue::Filter::testSelectStruct(const SelectWrapper& select) const { - if (select.isNpcOnly() && (mActor.getType() != ESM::NPC::sRecordId)) - // If the actor is a creature, we pass all conditions only applicable to NPCs. - return true; - if (select.getFunction() == ESM::DialogueCondition::Function_Choice && mChoice == -1) // If not currently in a choice, we reject all conditions that test against choices. return false; @@ -305,9 +301,13 @@ bool MWDialogue::Filter::testSelectStructNumeric(const SelectWrapper& select) co switch (select.getFunction()) { case ESM::DialogueCondition::Function_Global: - + { + const auto& world = MWBase::Environment::get().getWorld(); + if (world->getGlobalVariableType(select.getName()) == ' ') + return true; // ignore this filter if the global doesn't exist // internally all globals are float :( - return select.selectCompare(MWBase::Environment::get().getWorld()->getGlobalFloat(select.getName())); + return select.selectCompare(world->getGlobalFloat(select.getName())); + } case ESM::DialogueCondition::Function_Local: { @@ -504,7 +504,8 @@ int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) cons return MWBase::Environment::get().getWorld()->getCurrentWeather(); case ESM::DialogueCondition::Function_Reputation: - + if (!mActor.getClass().isNpc()) + return 0; return mActor.getClass().getNpcStats(mActor).getReputation(); case ESM::DialogueCondition::Function_FactionRankDifference: @@ -586,11 +587,11 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con case ESM::DialogueCondition::Function_NotClass: - return mActor.get()->mBase->mClass != select.getId(); + return !mActor.getClass().isNpc() || mActor.get()->mBase->mClass != select.getId(); case ESM::DialogueCondition::Function_NotRace: - return mActor.get()->mBase->mRace != select.getId(); + return !mActor.getClass().isNpc() || mActor.get()->mBase->mRace != select.getId(); case ESM::DialogueCondition::Function_NotCell: { @@ -598,12 +599,14 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con return !Misc::StringUtils::ciStartsWith(actorCell, select.getCellName()); } case ESM::DialogueCondition::Function_SameSex: - + if (!mActor.getClass().isNpc()) + return false; return (player.get()->mBase->mFlags & ESM::NPC::Female) == (mActor.get()->mBase->mFlags & ESM::NPC::Female); case ESM::DialogueCondition::Function_SameRace: - + if (!mActor.getClass().isNpc()) + return false; return mActor.get()->mBase->mRace == player.get()->mBase->mRace; case ESM::DialogueCondition::Function_SameFaction: @@ -668,7 +671,7 @@ bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) con case ESM::DialogueCondition::Function_Werewolf: - return mActor.getClass().getNpcStats(mActor).isWerewolf(); + return mActor.getClass().isNpc() && mActor.getClass().getNpcStats(mActor).isWerewolf(); default: diff --git a/apps/openmw/mwdialogue/keywordsearch.hpp b/apps/openmw/mwdialogue/keywordsearch.hpp index 2c98eac218..7032925321 100644 --- a/apps/openmw/mwdialogue/keywordsearch.hpp +++ b/apps/openmw/mwdialogue/keywordsearch.hpp @@ -3,6 +3,7 @@ #include // std::reverse #include +#include #include #include #include @@ -66,6 +67,8 @@ namespace MWDialogue return false; } + static bool isWordSeparator(char c) { return std::strchr("\n\r \t'\"", c) != nullptr; } + static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; } void highlightKeywords(Point beg, Point end, std::vector& out) const @@ -73,6 +76,14 @@ namespace MWDialogue std::vector matches; for (Point i = beg; i != end; ++i) { + if (i != beg) + { + Point prev = i; + --prev; + if (!isWordSeparator(*prev)) + continue; + } + // check first character typename Entry::childen_t::const_iterator candidate = mRoot.mChildren.find(Misc::StringUtils::toLower(*i)); @@ -86,7 +97,6 @@ namespace MWDialogue // some keywords might be longer variations of other keywords, so we definitely need a list of // candidates the first element in the pair is length of the match, i.e. depth from the first character - // on std::vector> candidates; while ((j + 1) != end) diff --git a/apps/openmw/mwdialogue/selectwrapper.cpp b/apps/openmw/mwdialogue/selectwrapper.cpp index 02c9d29b59..f9469bf9a9 100644 --- a/apps/openmw/mwdialogue/selectwrapper.cpp +++ b/apps/openmw/mwdialogue/selectwrapper.cpp @@ -247,29 +247,6 @@ MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const }; } -bool MWDialogue::SelectWrapper::isNpcOnly() const -{ - switch (mSelect.mFunction) - { - case ESM::DialogueCondition::Function_NotFaction: - case ESM::DialogueCondition::Function_NotClass: - case ESM::DialogueCondition::Function_NotRace: - case ESM::DialogueCondition::Function_SameSex: - case ESM::DialogueCondition::Function_SameRace: - case ESM::DialogueCondition::Function_SameFaction: - case ESM::DialogueCondition::Function_RankRequirement: - case ESM::DialogueCondition::Function_Reputation: - case ESM::DialogueCondition::Function_FactionRankDifference: - case ESM::DialogueCondition::Function_Werewolf: - case ESM::DialogueCondition::Function_PcWerewolfKills: - case ESM::DialogueCondition::Function_FacReactionLowest: - case ESM::DialogueCondition::Function_FacReactionHighest: - return true; - default: - return false; - } -} - bool MWDialogue::SelectWrapper::selectCompare(int value) const { return selectCompareImp(mSelect, value); diff --git a/apps/openmw/mwdialogue/selectwrapper.hpp b/apps/openmw/mwdialogue/selectwrapper.hpp index d831b6cea0..d15334cbe1 100644 --- a/apps/openmw/mwdialogue/selectwrapper.hpp +++ b/apps/openmw/mwdialogue/selectwrapper.hpp @@ -28,9 +28,6 @@ namespace MWDialogue Type getType() const; - bool isNpcOnly() const; - ///< \attention Do not call any of the select functions for this select struct! - bool selectCompare(int value) const; bool selectCompare(float value) const; 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/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index e14c400978..6b1e007770 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -588,10 +588,13 @@ namespace MWGui if (mTopicsList->getItemCount() > 0) mTopicsList->addSeparator(); + // Morrowind uses 3 px invisible borders for padding topics + constexpr int verticalPadding = 3; + for (const auto& keyword : mKeywords) { std::string topicId = Misc::StringUtils::lowerCase(keyword); - mTopicsList->addItem(keyword); + mTopicsList->addItem(keyword, verticalPadding); auto t = std::make_unique(keyword); mKeywordSearch.seed(topicId, intptr_t(t.get())); 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 a773b4635b..1b9f146284 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 @@ -206,7 +210,7 @@ namespace MWGui { mGuiMode = mode; const WindowSettingValues settings = getModeSettings(mGuiMode); - setPinButtonVisible(mode == GM_Inventory); + setPinButtonVisible(mode != GM_Container && mode != GM_Companion && mode != GM_Barter); const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; @@ -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) @@ -520,36 +545,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 +573,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 +617,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 +632,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 +688,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 +784,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 +863,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..2fbaa8d8ac 100644 --- a/apps/openmw/mwgui/jailscreen.cpp +++ b/apps/openmw/mwgui/jailscreen.cpp @@ -23,7 +23,6 @@ namespace MWGui : WindowBase("openmw_jail_screen.layout") , mDays(1) , mFadeTimeRemaining(0) - , mTimeAdvancer(0.01f) { getWidget(mProgressBar, "ProgressBar"); diff --git a/apps/openmw/mwgui/journalbooks.hpp b/apps/openmw/mwgui/journalbooks.hpp index 1970830eab..792edcc070 100644 --- a/apps/openmw/mwgui/journalbooks.hpp +++ b/apps/openmw/mwgui/journalbooks.hpp @@ -4,7 +4,7 @@ #include "bookpage.hpp" #include "journalviewmodel.hpp" -#include +#include namespace MWGui { diff --git a/apps/openmw/mwgui/journalwindow.hpp b/apps/openmw/mwgui/journalwindow.hpp index 22e7048acf..f0f394156c 100644 --- a/apps/openmw/mwgui/journalwindow.hpp +++ b/apps/openmw/mwgui/journalwindow.hpp @@ -3,7 +3,7 @@ #include "windowbase.hpp" -#include +#include #include 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/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index a59f225e9e..54f9ae4187 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -75,7 +75,7 @@ namespace MWGui int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mActor, x, true); std::string name{ iter->getClass().getName(*iter) }; - name += " - " + MyGUI::utility::toString(price) + name += " - " + MyGUI::utility::toString(price) + MWBase::Environment::get().getESMStore()->get().find("sgp")->mValue.getString(); items.emplace_back(name, price, *iter); diff --git a/apps/openmw/mwgui/pickpocketitemmodel.cpp b/apps/openmw/mwgui/pickpocketitemmodel.cpp index fa7bce449b..8756427589 100644 --- a/apps/openmw/mwgui/pickpocketitemmodel.cpp +++ b/apps/openmw/mwgui/pickpocketitemmodel.cpp @@ -101,8 +101,8 @@ namespace MWGui { MWBase::Environment::get().getMechanicsManager()->commitCrime( player, mActor, MWBase::MechanicsManager::OT_Pickpocket, ESM::RefId(), 0, true); - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); } } @@ -129,8 +129,8 @@ namespace MWGui { MWBase::Environment::get().getMechanicsManager()->commitCrime( player, mActor, MWBase::MechanicsManager::OT_Pickpocket, ESM::RefId(), 0, true); - MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); return false; } else diff --git a/apps/openmw/mwgui/postprocessorhud.cpp b/apps/openmw/mwgui/postprocessorhud.cpp index 8f5b20ba98..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 || 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,25 +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); - - if (!technique) - continue; + 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); } } @@ -484,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 93b0ef071f..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); @@ -421,6 +419,9 @@ namespace MWGui } store.setSelectedEnchantItem(it); + // to reset WindowManager::mSelectedSpell immediately + MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } } @@ -448,6 +449,9 @@ namespace MWGui store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } + + // Updates the state of equipped/not equipped (skin) in spellwindow + MWBase::Environment::get().getWindowManager()->updateSpellWindow(); } // --------------------------------------------------------------------------------------------------------- @@ -566,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 94f25e118b..eec3b7bfe6 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -43,6 +43,7 @@ namespace MWGui { getWidget(mScreenshot, "Screenshot"); getWidget(mCharacterSelection, "SelectCharacter"); + getWidget(mCellName, "CellName"); getWidget(mInfoText, "InfoText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); @@ -196,12 +197,12 @@ namespace MWGui title << " (#{OMWEngine:Level} " << signature.mPlayerLevel << " " << MyGUI::TextIterator::toTagsString(MyGUI::UString(className)) << ")"; - mCharacterSelection->addItem(MyGUI::LanguageManager::getInstance().replaceTags(title.str())); + const MyGUI::UString playerDesc = MyGUI::LanguageManager::getInstance().replaceTags(title.str()); + mCharacterSelection->addItem(playerDesc, &*it); if (mCurrentCharacter == &*it || (!mCurrentCharacter && !mSaving - && Misc::StringUtils::ciEqual( - directory, Files::pathToUnicodeString(it->begin()->mPath.parent_path().filename())))) + && Misc::StringUtils::ciEqual(directory, Files::pathToUnicodeString(it->getPath().filename())))) { mCurrentCharacter = &*it; selectedIndex = mCharacterSelection->getItemCount() - 1; @@ -209,6 +210,11 @@ namespace MWGui } } + if (selectedIndex == MyGUI::ITEM_NONE && !mSaving && mCharacterSelection->getItemCount() != 0) + { + selectedIndex = 0; + mCurrentCharacter = *mCharacterSelection->getItemDataAt(0); + } mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) mCharacterSelection->setCaptionWithReplacing("#{OMWEngine:SelectCharacter}"); @@ -320,16 +326,7 @@ namespace MWGui void SaveGameDialog::onCharacterSelected(MyGUI::ComboBox* sender, size_t pos) { - MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); - - unsigned int i = 0; - const MWState::Character* character = nullptr; - for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it, ++i) - { - if (i == pos) - character = &*it; - } - assert(character && "Can't find selected character"); + const MWState::Character* character = *mCharacterSelection->getItemDataAt(pos); mCurrentCharacter = character; mCurrentSlot = nullptr; @@ -390,6 +387,7 @@ namespace MWGui if (pos == MyGUI::ITEM_NONE || !mCurrentCharacter) { mCurrentSlot = nullptr; + mCellName->setCaption({}); mInfoText->setCaption({}); mScreenshot->setImageTexture({}); return; @@ -399,7 +397,7 @@ namespace MWGui mSaveNameEdit->setCaption(sender->getItemNameAt(pos)); mCurrentSlot = nullptr; - unsigned int i = 0; + size_t i = 0; for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it, ++i) { @@ -411,15 +409,22 @@ namespace MWGui std::stringstream text; - text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n"; + const size_t profileIndex = mCharacterSelection->getIndexSelected(); + const std::string& slotPlayerName = mCurrentSlot->mProfile.mPlayerName; + const ESM::SavedGame& profileSavedGame + = (*mCharacterSelection->getItemDataAt(profileIndex))->getSignature(); + if (slotPlayerName != profileSavedGame.mPlayerName) + text << slotPlayerName << "\n"; + + text << "#{OMWEngine:Level} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; + + if (mCurrentSlot->mProfile.mCurrentDay > 0) + text << "#{Calendar:day} " << mCurrentSlot->mProfile.mCurrentDay << "\n"; if (mCurrentSlot->mProfile.mMaximumHealth > 0) text << "#{OMWEngine:Health} " << static_cast(mCurrentSlot->mProfile.mCurrentHealth) << "/" << static_cast(mCurrentSlot->mProfile.mMaximumHealth) << "\n"; - text << "#{OMWEngine:Level} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; - text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCellName << "}\n"; - int hour = int(mCurrentSlot->mProfile.mInGameTime.mGameHour); bool pm = hour >= 12; if (hour >= 13) @@ -427,20 +432,19 @@ namespace MWGui if (hour == 0) hour = 12; - if (mCurrentSlot->mProfile.mCurrentDay > 0) - text << "#{Calendar:day} " << mCurrentSlot->mProfile.mCurrentDay << "\n"; - text << mCurrentSlot->mProfile.mInGameTime.mDay << " " << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName( mCurrentSlot->mProfile.mInGameTime.mMonth) - << " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}"); + << " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}") << "\n"; if (mCurrentSlot->mProfile.mTimePlayed > 0) { - text << "\n" - << "#{OMWEngine:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed); + text << "#{OMWEngine:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed) << "\n"; } + text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n"; + + mCellName->setCaptionWithReplacing("#{sCell=" + mCurrentSlot->mProfile.mPlayerCellName + "}"); mInfoText->setCaptionWithReplacing(text.str()); // Reset the image for the case we're unable to recover a screenshot @@ -484,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/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index 35e65fbed0..af831f066e 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -57,6 +57,7 @@ namespace MWGui bool mSaving; MyGUI::ComboBox* mCharacterSelection; + MyGUI::EditBox* mCellName; MyGUI::EditBox* mInfoText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; 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/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index 9fca86caba..2a67af5498 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -19,6 +19,7 @@ #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spells.hpp" +#include "../mwmechanics/spellutil.hpp" namespace MWGui { @@ -43,8 +44,8 @@ namespace MWGui const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); int price = std::max(1, - static_cast( - spell.mData.mCost * store.get().find("fSpellValueMult")->mValue.getFloat())); + static_cast(MWMechanics::calcSpellCost(spell) + * store.get().find("fSpellValueMult")->mValue.getFloat())); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -62,7 +63,7 @@ namespace MWGui mCurrentY += lineHeight; toAdd->setUserData(price); - toAdd->setCaptionWithReplacing(spell.mName + " - " + MyGUI::utility::toString(price) + "#{sgp}"); + toAdd->setCaptionWithReplacing(spell.mName + " - " + MyGUI::utility::toString(price) + "#{sgp}"); toAdd->setSize(mSpellsView->getWidth(), lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &SpellBuyingWindow::onMouseWheel); toAdd->setUserString("ToolTipType", "Spell"); diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index d183a00273..566b7f4ccd 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -242,21 +242,50 @@ namespace MWGui return; mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); - - SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); - if (selected < 0) - selected = 0; - - selected += next ? 1 : -1; - int itemcount = mSpellView->getModel()->getItemCount(); - if (itemcount == 0) + int itemCount = mSpellView->getModel()->getItemCount(); + if (itemCount == 0) return; - selected = (selected + itemcount) % itemcount; - const Spell& spell = mSpellView->getModel()->getItem(selected); - if (spell.mType == Spell::Type_EnchantedItem) - onEnchantedItemSelected(spell.mItem, spell.mActive); + SpellModel::ModelIndex nextIndex; + SpellModel::ModelIndex currentIndex = mSpellView->getModel()->getSelectedIndex(); + + // If we have a selected index, search for a valid selection in the target direction + if (currentIndex >= 0) + { + MWWorld::ContainerStore store; + const Spell& currentSpell = mSpellView->getModel()->getItem(currentIndex); + + nextIndex = currentIndex; + for (int i = 0; i < itemCount; i++) + { + nextIndex += next ? 1 : -1; + nextIndex = (nextIndex + itemCount) % itemCount; + + // We can keep this selection if: + // * we're not switching off of an enchanted item + // * we're not switching to an enchanted item + // * the next item wouldn't stack with the current item + if (currentSpell.mType != Spell::Type_EnchantedItem) + break; + + const Spell& nextSpell = mSpellView->getModel()->getItem(nextIndex); + if (nextSpell.mType != Spell::Type_EnchantedItem || !store.stacks(currentSpell.mItem, nextSpell.mItem)) + break; + } + } + // Otherwise, the first selection is always index 0 else - onSpellSelected(spell.mId); + nextIndex = 0; + + // Only trigger the selection event if the selection is actually changing. + // The itemCount check earlier ensures we have at least one spell to select. + if (nextIndex != currentIndex) + { + const Spell& selectedSpell = mSpellView->getModel()->getItem(nextIndex); + if (selectedSpell.mType == Spell::Type_EnchantedItem) + onEnchantedItemSelected(selectedSpell.mItem, selectedSpell.mActive); + else + onSpellSelected(selectedSpell.mId); + } } } 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..4bde77a552 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -27,7 +27,6 @@ namespace MWGui TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") - , mTimeAdvancer(0.05f) { getWidget(mTrainingOptions, "TrainingOptions"); getWidget(mCancelButton, "CancelButton"); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index 6ba3a55286..e001cf9b43 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -33,15 +34,9 @@ namespace MWGui { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); - getWidget(mSelect, "Select"); - getWidget(mDestinations, "Travel"); getWidget(mDestinationsView, "DestinationsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onCancelButtonClicked); - - mDestinations->setCoord(450 / 2 - mDestinations->getTextSize().width / 2, mDestinations->getTop(), - mDestinations->getTextSize().width, mDestinations->getHeight()); - mSelect->setCoord(8, mSelect->getTop(), mSelect->getTextSize().width, mSelect->getHeight()); } void TravelWindow::addDestination(const ESM::RefId& name, const ESM::Position& pos, bool interior) @@ -70,9 +65,6 @@ namespace MWGui price = static_cast(d); } - price = std::max(1, price); - price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); - // Add price for the travelling followers std::set followers; MWWorld::ActionTeleport::getFollowers(player, followers, !interior); @@ -80,6 +72,9 @@ namespace MWGui // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); + price = std::max(1, price); + price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); + const int lineHeight = Settings::gui().mFontSize + 2; MyGUI::Button* toAdd = mDestinationsView->createWidget( @@ -93,8 +88,7 @@ namespace MWGui const std::string& nameString = name.getRefIdString(); toAdd->setUserString("price", std::to_string(price)); - toAdd->setCaptionWithReplacing( - "#{sCell=" + nameString + "} - " + MyGUI::utility::toString(price) + "#{sgp}"); + toAdd->setCaptionWithReplacing("#{sCell=" + nameString + "} - " + MyGUI::utility::toString(price) + "#{sgp}"); toAdd->setSize(mDestinationsView->getWidth(), lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); toAdd->setUserString("Destination", nameString); @@ -131,12 +125,23 @@ namespace MWGui bool interior = true; const ESM::ExteriorCellLocation cellIndex = ESM::positionToExteriorCellLocation(dest.mPos.pos[0], dest.mPos.pos[1]); + const MWWorld::WorldModel& worldModel = *MWBase::Environment::get().getWorldModel(); if (cellname.empty()) { - MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); + MWWorld::CellStore& cell = worldModel.getExterior(cellIndex); cellname = MWBase::Environment::get().getWorld()->getCellName(&cell); interior = false; } + else + { + const MWWorld::CellStore* destCell = worldModel.findCell(cellname, false); + if (destCell == nullptr) + { + Log(Debug::Error) << "Failed to add travel destination: unknown cell (" << cellname << ")"; + continue; + } + interior = !destCell->getCell()->isExterior(); + } addDestination(ESM::RefId::stringRefId(cellname), dest.mPos, interior); } diff --git a/apps/openmw/mwgui/travelwindow.hpp b/apps/openmw/mwgui/travelwindow.hpp index 6d7c1c7376..630e27518a 100644 --- a/apps/openmw/mwgui/travelwindow.hpp +++ b/apps/openmw/mwgui/travelwindow.hpp @@ -24,8 +24,6 @@ namespace MWGui protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; - MyGUI::TextBox* mDestinations; - MyGUI::TextBox* mSelect; MyGUI::ScrollView* mDestinationsView; 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 568f05abc3..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) @@ -84,15 +83,29 @@ namespace MWGui void WaitDialog::setPtr(const MWWorld::Ptr& ptr) { - setCanRest(!ptr.isEmpty() || MWBase::Environment::get().getWorld()->canRest() == MWBase::World::Rest_Allowed); + const int restFlags = MWBase::Environment::get().getWorld()->canRest(); - if (ptr.isEmpty() && MWBase::Environment::get().getWorld()->canRest() == MWBase::World::Rest_PlayerIsInAir) + const bool underwater = (restFlags & MWBase::World::Rest_PlayerIsUnderwater) != 0; + // Resting in air is allowed if you're using a bed + const bool inAir = ptr.isEmpty() && (restFlags & MWBase::World::Rest_PlayerIsInAir) != 0; + const bool enemiesNearby = (restFlags & MWBase::World::Rest_EnemiesAreNearby) != 0; + const bool solidGround = !underwater && !inAir; + + if (!solidGround || enemiesNearby) { - // Resting in air is not allowed unless you're using a bed - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); - MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); + if (!solidGround) + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); + + if (enemiesNearby) + MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); + + MWBase::Environment::get().getWindowManager()->popGuiMode(); + return; } + const bool canSleep = !ptr.isEmpty() || (restFlags & MWBase::World::Rest_CanSleep) != 0; + setCanRest(canSleep); + if (mUntilHealedButton->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mUntilHealedButton); else @@ -138,20 +151,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->popGuiMode(); } - MWBase::World::RestPermitted canRest = MWBase::Environment::get().getWorld()->canRest(); - - if (canRest == MWBase::World::Rest_EnemiesAreNearby) - { - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); - MWBase::Environment::get().getWindowManager()->popGuiMode(); - } - else if (canRest == MWBase::World::Rest_PlayerIsUnderwater) - { - // resting underwater not allowed - MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); - MWBase::Environment::get().getWindowManager()->popGuiMode(); - } - onHourSliderChangedPosition(mHourSlider, 0); mHourSlider->setScrollPosition(0); 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 03902e21c4..c231718db6 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -8,7 +8,6 @@ **/ #include -#include #include #include @@ -22,7 +21,7 @@ #include #include #include -#include +#include #include "charactercreation.hpp" #include "draganddrop.hpp" @@ -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/README.md b/apps/openmw/mwlua/README.md index ed911eb01d..a1cfeb1f1b 100644 --- a/apps/openmw/mwlua/README.md +++ b/apps/openmw/mwlua/README.md @@ -3,7 +3,7 @@ This folder contains the C++ implementation of the Lua scripting system. For user-facing documentation, see -[OpenMW Lua scripting](https://openmw.readthedocs.io/en/latest/reference/lua-scripting/index.html). +[Lua scripting](https://openmw.readthedocs.io/en/latest/reference/lua-scripting/index.html). The documentation is generated from [/docs/source/reference/lua-scripting](/docs/source/reference/lua-scripting). You can find instructions for generating the documentation at the 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 9df435c00d..b85c14578c 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -19,8 +19,10 @@ #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" +#include "coremwscriptbindings.hpp" #include "dialoguebindings.hpp" #include "factionbindings.hpp" +#include "landbindings.hpp" #include "luaevents.hpp" #include "magicbindings.hpp" #include "soundbindings.hpp" @@ -97,6 +99,11 @@ namespace MWLua api["stats"] = context.cachePackage("openmw_core_stats", [context]() { return initCoreStatsBindings(context); }); + api["mwscripts"] + = context.cachePackage("openmw_core_mwscripts", [context]() { return initCoreMwScriptBindings(context); }); + + api["land"] = context.cachePackage("openmw_core_land", [context]() { return initCoreLandBindings(context); }); + api["factions"] = context.cachePackage("openmw_core_factions", [context]() { return initCoreFactionBindings(context); }); api["dialogue"] diff --git a/apps/openmw/mwlua/coremwscriptbindings.cpp b/apps/openmw/mwlua/coremwscriptbindings.cpp new file mode 100644 index 0000000000..6bf01a4b3d --- /dev/null +++ b/apps/openmw/mwlua/coremwscriptbindings.cpp @@ -0,0 +1,28 @@ +#include "coremwscriptbindings.hpp" + +#include + +#include "../mwworld/esmstore.hpp" + +#include "context.hpp" +#include "recordstore.hpp" + +namespace MWLua +{ + sol::table initCoreMwScriptBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table api(lua, sol::create); + + auto recordBindingsClass = lua.new_usertype("ESM3_Script"); + recordBindingsClass[sol::meta_function::to_string] + = [](const ESM::Script& rec) { return "ESM3_Script[" + rec.mId.toDebugString() + "]"; }; + recordBindingsClass["id"] + = sol::readonly_property([](const ESM::Script& rec) { return rec.mId.serializeText(); }); + recordBindingsClass["text"] = sol::readonly_property([](const ESM::Script& rec) { return rec.mScriptText; }); + + addRecordFunctionBinding(api, context); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/coremwscriptbindings.hpp b/apps/openmw/mwlua/coremwscriptbindings.hpp new file mode 100644 index 0000000000..0f69b7dcb7 --- /dev/null +++ b/apps/openmw/mwlua/coremwscriptbindings.hpp @@ -0,0 +1,13 @@ +#ifndef MWLUA_COREMWSCRIPTBINDINGS_H +#define MWLUA_COREMWSCRIPTBINDINGS_H + +#include + +namespace MWLua +{ + struct Context; + + sol::table initCoreMwScriptBindings(const Context& context); +} + +#endif // MWLUA_COREMWSCRIPTBINDINGS_H diff --git a/apps/openmw/mwlua/landbindings.cpp b/apps/openmw/mwlua/landbindings.cpp new file mode 100644 index 0000000000..3f85e2b066 --- /dev/null +++ b/apps/openmw/mwlua/landbindings.cpp @@ -0,0 +1,124 @@ +#include "landbindings.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/worldmodel.hpp" +#include "object.hpp" + +namespace +{ + // Takes in a corrected world pos to match the visuals. + ESMTerrain::UniqueTextureId getTextureAt(const std::span landData, const int plugin, + const osg::Vec3f& correctedWorldPos, const float cellSize) + { + int cellX = static_cast(std::floor(correctedWorldPos.x() / cellSize)); + int cellY = static_cast(std::floor(correctedWorldPos.y() / cellSize)); + + // Normalized position in the cell + float nX = (correctedWorldPos.x() - (cellX * cellSize)) / cellSize; + float nY = (correctedWorldPos.y() - (cellY * cellSize)) / cellSize; + + int startX = static_cast(nX * ESM::Land::LAND_TEXTURE_SIZE); + int startY = static_cast(nY * ESM::Land::LAND_TEXTURE_SIZE); + + assert(startX < ESM::Land::LAND_TEXTURE_SIZE); + assert(startY < ESM::Land::LAND_TEXTURE_SIZE); + + const std::uint16_t tex = landData[startY * ESM::Land::LAND_TEXTURE_SIZE + startX]; + if (tex == 0) + return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin + + return { tex, plugin }; + } + + const ESM::RefId worldspaceAt(sol::object cellOrId) + { + const MWWorld::Cell* cell = nullptr; + if (cellOrId.is()) + cell = cellOrId.as().mStore->getCell(); + else if (cellOrId.is()) + cell = cellOrId.as().mStore->getCell(); + else if (cellOrId.is() && !cellOrId.as().empty()) + cell = MWBase::Environment::get() + .getWorldModel() + ->getCell(ESM::RefId::deserializeText(cellOrId.as())) + .getCell(); + if (cell == nullptr) + throw std::runtime_error("Invalid cell"); + else if (!cell->isExterior()) + throw std::runtime_error("Cell cannot be interior"); + + return cell->getWorldSpace(); + } +} + +namespace MWLua +{ + sol::table initCoreLandBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table landApi(lua, sol::create); + + landApi["getHeightAt"] = [](const osg::Vec3f& pos, sol::object cellOrId) { + ESM::RefId worldspace = worldspaceAt(cellOrId); + return MWBase::Environment::get().getWorld()->getTerrainHeightAt(pos, worldspace); + }; + + landApi["getTextureAt"] = [lua = lua](const osg::Vec3f& pos, sol::object cellOrId) { + sol::variadic_results values; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const MWWorld::Store& landStore = store.get(); + ESM::RefId worldspace = worldspaceAt(cellOrId); + + if (worldspace != ESM::Cell::sDefaultWorldspaceId) + return values; + + const float cellSize = ESM::getCellSize(worldspace); + const float offset = (cellSize / ESM::LandRecordData::sLandTextureSize) * 0.25; + const osg::Vec3f correctedPos = pos + osg::Vec3f{ -offset, +offset, 0.0f }; + + const ESM::Land* land = nullptr; + const ESM::Land::LandData* landData = nullptr; + + int cellX = static_cast(std::floor(correctedPos.x() / cellSize)); + int cellY = static_cast(std::floor(correctedPos.y() / cellSize)); + + land = landStore.search(cellX, cellY); + + if (land != nullptr) + landData = land->getLandData(ESM::Land::DATA_VTEX); + + // If we fail to preload land data, return, we need to be able to get *any* land to know how to correct + // the position used to sample terrain + if (landData == nullptr) + return values; + + const ESMTerrain::UniqueTextureId textureId + = getTextureAt(landData->mTextures, land->getPlugin(), correctedPos, cellSize); + + // Need to check for 0, 0 so that we can safely subtract 1 later, as per documentation on UniqueTextureId + if (textureId.first != 0) + { + const MWWorld::Store& textureStore = store.get(); + const std::string* textureString = textureStore.search(textureId.first - 1, textureId.second); + if (!textureString) + return values; + + values.push_back(sol::make_object(lua, *textureString)); + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + if (textureId.second >= 0 && static_cast(textureId.second) < contentList.size()) + values.push_back(sol::make_object(lua, contentList[textureId.second])); + } + + return values; + }; + + return LuaUtil::makeReadOnly(landApi); + } +} diff --git a/apps/openmw/mwlua/landbindings.hpp b/apps/openmw/mwlua/landbindings.hpp new file mode 100644 index 0000000000..8cdf30046b --- /dev/null +++ b/apps/openmw/mwlua/landbindings.hpp @@ -0,0 +1,11 @@ +#ifndef MWLUA_LANDBINDINGS_H +#define MWLUA_LANDBINDINGS_H + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreLandBindings(const Context& context); +} + +#endif // MWLUA_LANDBINDINGS_H diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 4bdfb0d13a..d784328035 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -235,13 +235,16 @@ namespace MWLua &mOnSkillLevelUp }); } - 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..146eff95ba 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -67,7 +67,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); } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 5fa2d9867c..2fd7618ad7 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -540,7 +540,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..9877c98fb9 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. // diff --git a/apps/openmw/mwlua/postprocessingbindings.cpp b/apps/openmw/mwlua/postprocessingbindings.cpp index e64bf0fa9e..f12bda8650 100644 --- a/apps/openmw/mwlua/postprocessingbindings.cpp +++ b/apps/openmw/mwlua/postprocessingbindings.cpp @@ -1,5 +1,7 @@ #include "postprocessingbindings.hpp" +#include "MyGUI_LanguageManager.h" + #include #include "../mwbase/environment.hpp" @@ -8,6 +10,14 @@ #include "luamanagerimp.hpp" +namespace +{ + std::string getLocalizedMyGUIString(std::string_view unlocalized) + { + return MyGUI::LanguageManager::getInstance().replaceTags(std::string(unlocalized)).asUTF8(); + } +} + namespace MWLua { struct Shader; @@ -25,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)) { } @@ -37,7 +47,7 @@ namespace MWLua if (!mShader) return "Shader(nil)"; - return Misc::StringUtils::format("Shader(%s, %s)", mShader->getName(), mShader->getFileName()); + return Misc::StringUtils::format("Shader(%s, %s)", mShader->getName(), mShader->getFileName().value()); } enum @@ -139,6 +149,15 @@ namespace MWLua return MWBase::Environment::get().getWorld()->getPostProcessor()->isTechniqueEnabled(shader.mShader); }; + shader["name"] = sol::readonly_property( + [](const Shader& shader) { return getLocalizedMyGUIString(shader.mShader->getName()); }); + shader["author"] = sol::readonly_property( + [](const Shader& shader) { return getLocalizedMyGUIString(shader.mShader->getAuthor()); }); + shader["description"] = sol::readonly_property( + [](const Shader& shader) { return getLocalizedMyGUIString(shader.mShader->getDescription()); }); + shader["version"] = sol::readonly_property( + [](const Shader& shader) { return getLocalizedMyGUIString(shader.mShader->getVersion()); }); + shader["setBool"] = getSetter(context); shader["setFloat"] = getSetter(context); shader["setInt"] = getSetter(context); @@ -158,12 +177,23 @@ namespace MWLua if (!shader.mShader || !shader.mShader->isValid()) throw std::runtime_error(Misc::StringUtils::format("Failed loading shader '%s'", name)); - if (!shader.mShader->getDynamic()) - throw std::runtime_error(Misc::StringUtils::format("Shader '%s' is not marked as dynamic", name)); - return shader; }; + api["getChain"] = [context]() { + sol::table chain(context.sol(), sol::create); + + for (const auto& shader : MWBase::Environment::get().getWorld()->getPostProcessor()->getChain()) + { + // Don't expose internal shaders to the API, they should be invisible to the user + if (shader->getInternal()) + continue; + chain.add(Shader(shader)); + } + + return chain; + }; + return LuaUtil::makeReadOnly(api); } 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/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..826338ca7d 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -296,6 +296,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/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 9e64996e46..270faf8598 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -267,19 +267,20 @@ 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 }); + mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true }); mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } } bool updateSpellWindow = false; + bool playNonLooping = false; if (ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) { auto& store = ptr.getClass().getInventoryStore(ptr); if (store.getInvListener() != nullptr) { - bool playNonLooping = !store.isFirstEquip(); + playNonLooping = !store.isFirstEquip(); const auto world = MWBase::Environment::get().getWorld(); for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) { @@ -307,9 +308,6 @@ namespace MWMechanics 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; } } @@ -327,7 +325,7 @@ namespace MWMechanics std::optional reflected; for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { - auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); + auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, playNonLooping); if (result.mType == MagicApplicationResult::Type::REFLECTED) { if (!reflected) @@ -506,9 +504,9 @@ namespace MWMechanics mQueue.emplace_back(params); } - void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) + void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) { - mQueue.emplace_back(ActiveSpellParams{ spell, actor, true }); + mQueue.emplace_back(ActiveSpellParams{ spell, actor, ignoreResistances }); } void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr) diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index e4fa60ddb6..3e4dafdb26 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -137,8 +137,8 @@ namespace MWMechanics /// void addSpell(const ActiveSpellParams& params); - /// Bypasses resistances - void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); + /// Force resistances + void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = true); /// Removes the active effects from this spell/potion/.. with \a id void removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index d7438712d9..69e370ec86 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 }; 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..3ba6bfdc8d 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -122,6 +122,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 +347,7 @@ namespace MWMechanics // Find something nearby. for (const Actor& otherActor : actors) { - if (otherActor.getPtr() == ptr) + if (otherActor.isInvalid() || otherActor.getPtr() == ptr) continue; updateHeadTracking( @@ -605,10 +607,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 +683,7 @@ namespace MWMechanics } } - if (creatureStats2.getMagicEffects().getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0) + if (isTargetMagicallyHidden(actor2)) return; // Stop here if target is unreachable @@ -1066,9 +1064,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 +1197,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 +1249,7 @@ namespace MWMechanics { if (!keepActive) removeTemporaryEffects(iter->second->getPtr()); - mActors.erase(iter->second); + iter->second->invalidate(); mIndex.erase(iter); } } @@ -1300,16 +1301,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; } } @@ -1328,6 +1328,8 @@ namespace MWMechanics const MWBase::World* const world = MWBase::Environment::get().getWorld(); for (const Actor& actor : mActors) { + if (actor.isInvalid()) + continue; const MWWorld::Ptr& ptr = actor.getPtr(); if (ptr == player) continue; // Don't interfere with player controls. @@ -1392,6 +1394,8 @@ namespace MWMechanics // Iterate through all other actors and predict collisions. for (const Actor& otherActor : mActors) { + if (otherActor.isInvalid()) + continue; const MWWorld::Ptr& otherPtr = otherActor.getPtr(); if (otherPtr == ptr || otherPtr == currentTarget) continue; @@ -1510,6 +1514,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 +1577,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 +1636,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 +1703,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 +1761,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 +1850,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 +1871,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 +2070,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 +2087,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 +2098,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 +2113,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; @@ -2353,10 +2385,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..a6f9935194 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -104,10 +104,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 +124,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 @@ -386,13 +393,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++) + size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); + for (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]); } } diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index ca33f5dc90..edb62c97c4 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -119,6 +119,7 @@ namespace MWMechanics /// Reset pathfinding state void reset(); + virtual void resetInitialPosition() {} /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise /// actor should rotate while standing. diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 019aaf7c0a..70b94b1eba 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -400,7 +400,7 @@ namespace MWMechanics const auto newTypeId = package.getTypeId(); if (currentTypeId <= MWMechanics::AiPackageTypeId::Wander && !hasPackage(MWMechanics::AiPackageTypeId::InternalTravel) - && (newTypeId <= MWMechanics::AiPackageTypeId::Combat || newTypeId == MWMechanics::AiPackageTypeId::Pursue + && (newTypeId == MWMechanics::AiPackageTypeId::Combat || newTypeId == MWMechanics::AiPackageTypeId::Pursue || newTypeId == MWMechanics::AiPackageTypeId::Cast)) { osg::Vec3f dest; @@ -444,8 +444,15 @@ namespace MWMechanics if ((*it)->getPriority() <= package.getPriority()) { + if (cancelOther && isActualAiPackage((*it)->getTypeId())) + mAiState.reset(); onPackageAdded(package); - mPackages.insert(it, package.clone()); + it = mPackages.insert(it, package.clone()); + if (newTypeId == MWMechanics::AiPackageTypeId::Follow) + { + for (++it; it != mPackages.end(); ++it) + (*it)->resetInitialPosition(); + } return; } } @@ -455,11 +462,7 @@ namespace MWMechanics // Make sure that temporary storage is empty if (cancelOther) - { - mAiState.moveIn(std::make_unique()); - mAiState.moveIn(std::make_unique()); - mAiState.moveIn(std::make_unique()); - } + mAiState.reset(); } bool MWMechanics::AiSequence::isEmpty() const diff --git a/apps/openmw/mwmechanics/aistate.hpp b/apps/openmw/mwmechanics/aistate.hpp index f2ce17fd9c..bb88557b6d 100644 --- a/apps/openmw/mwmechanics/aistate.hpp +++ b/apps/openmw/mwmechanics/aistate.hpp @@ -59,12 +59,7 @@ namespace MWMechanics mStorage = std::make_unique(payload); } - /// \brief takes ownership of the passed object - template - void moveIn(std::unique_ptr&& storage) - { - mStorage = std::move(storage); - } + void reset() { mStorage.reset(); } }; } diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 42667a406a..3cc7aac838 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -676,6 +676,13 @@ namespace MWMechanics stopMovement(actor); } + void AiWander::resetInitialPosition() + { + mStoredInitialActorPosition = false; + mPathFinder.clearPath(); + mHasDestination = false; + } + bool AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) @@ -804,7 +811,7 @@ namespace MWMechanics converter.toWorld(dest); - state.moveIn(std::make_unique()); + state.reset(); osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); MWBase::Environment::get().getWorld()->moveObject(actor, pos); @@ -820,7 +827,7 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } @@ -853,7 +860,7 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); // mAllowedNodes for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 01a02096a4..f08980ad29 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -122,6 +122,8 @@ namespace MWMechanics static std::string_view getIdleGroupName(size_t index) { return sIdleSelectToGroupName[index]; } + void resetInitialPosition() override; + private: void stopWalking(const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 87cc469eb4..1aac063ce3 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(); @@ -2528,7 +2537,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 +2750,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 +2870,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 +2932,7 @@ namespace MWMechanics bool CharacterController::isReadyToBlock() const { - return updateCarriedLeftVisible(mWeaponType); + return mAnimation && updateCarriedLeftVisible(mWeaponType); } bool CharacterController::isKnockedDown() const @@ -3030,7 +3036,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 +3068,8 @@ namespace MWMechanics float CharacterController::getAnimationMovementDirection() const { + if (!mAnimation) + return 0.f; switch (mMovementState) { case CharState_RunLeft: @@ -3155,6 +3164,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/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index c793d5d540..262f813916 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -66,7 +66,9 @@ namespace MWMechanics if (Misc::Rng::rollDice(10000, prng) < x) { // Contracted disease! - actor.getClass().getCreatureStats(actor).getSpells().add(spell); + MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); + creatureStats.getSpells().add(spell); + creatureStats.getActiveSpells().addSpell(spell, actor, false); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); std::string msg = MWBase::Environment::get() diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 7d0007f9e3..66bef89e2c 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -185,18 +185,18 @@ namespace MWMechanics * * Formula on UESPWiki is not entirely correct. */ - float Enchanting::getEnchantPoints(bool precise) const + std::vector Enchanting::getEffectCosts() const { + std::vector costs; if (mEffectList.mList.empty()) - // No effects added, cost = 0 - return 0; + return costs; + costs.reserve(mEffectList.mList.size()); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); const float fEnchantmentConstantDurationMult = store.get().find("fEnchantmentConstantDurationMult")->mValue.getFloat(); - float enchantmentCost = 0.f; float cost = 0.f; for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { @@ -215,9 +215,18 @@ namespace MWMechanics if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; - enchantmentCost += precise ? cost : std::floor(cost); + costs.push_back(cost); } + return costs; + } + + float Enchanting::getEnchantPoints(bool precise) const + { + float enchantmentCost = 0.f; + for (float cost : getEffectCosts()) + enchantmentCost += precise ? cost : std::floor(cost); + return enchantmentCost; } @@ -278,13 +287,19 @@ namespace MWMechanics if (mEnchanter.isEmpty()) return 0; + // Use the final effect's accumulated cost + float finalEffectCost = 0.f; + std::vector effectCosts = getEffectCosts(); + if (!effectCosts.empty()) + finalEffectCost = effectCosts.back(); + float priceMultipler = MWBase::Environment::get() .getESMStore() ->get() .find("fEnchantmentValueMult") ->mValue.getFloat(); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer( - mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); + mEnchanter, static_cast(finalEffectCost * priceMultipler), true); price *= count * getTypeMultiplier(); return std::max(1, price); } diff --git a/apps/openmw/mwmechanics/enchanting.hpp b/apps/openmw/mwmechanics/enchanting.hpp index 5db02b8cba..98e0982f7c 100644 --- a/apps/openmw/mwmechanics/enchanting.hpp +++ b/apps/openmw/mwmechanics/enchanting.hpp @@ -2,6 +2,7 @@ #define GAME_MWMECHANICS_ENCHANTING_H #include +#include #include #include @@ -32,6 +33,7 @@ namespace MWMechanics float getTypeMultiplier() const; void payForEnchantment(int count) const; int getEnchantPrice(int count) const; + std::vector getEffectCosts() const; public: Enchanting(); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 47c49a8861..c20061d022 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) @@ -1232,33 +1243,39 @@ namespace MWMechanics victim.getClass().getCreatureStats(victim).notifyMurder(); // Bounty and disposition penalty for each type of crime - float disp = 0.f, dispVictim = 0.f; + int bounty; + float disp, dispVictim; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) { - arg = store.find("iCrimeTresspass")->mValue.getInteger(); + bounty = store.find("iCrimeTresspass")->mValue.getInteger(); disp = dispVictim = store.find("iDispTresspass")->mValue.getFloat(); } else if (type == OT_Pickpocket) { - arg = store.find("iCrimePickPocket")->mValue.getInteger(); + bounty = store.find("iCrimePickPocket")->mValue.getInteger(); disp = dispVictim = store.find("fDispPickPocketMod")->mValue.getFloat(); } else if (type == OT_Assault) { - arg = store.find("iCrimeAttack")->mValue.getInteger(); + bounty = store.find("iCrimeAttack")->mValue.getInteger(); disp = store.find("iDispAttackMod")->mValue.getFloat(); dispVictim = store.find("fDispAttacking")->mValue.getFloat(); } else if (type == OT_Murder) { - arg = store.find("iCrimeKilling")->mValue.getInteger(); + bounty = store.find("iCrimeKilling")->mValue.getInteger(); disp = dispVictim = store.find("iDispKilling")->mValue.getFloat(); } else if (type == OT_Theft) { + bounty = static_cast(arg * store.find("fCrimeStealing")->mValue.getFloat()); + bounty = std::max(1, bounty); // Minimum bounty of 1, in case items with zero value are stolen disp = dispVictim = store.find("fDispStealing")->mValue.getFloat() * arg; - arg = static_cast(arg * store.find("fCrimeStealing")->mValue.getFloat()); - arg = std::max(1, arg); // Minimum bounty of 1, in case items with zero value are stolen + } + else + { + bounty = arg; + disp = dispVictim = 0.f; } // Make surrounding actors within alarm distance respond to the crime @@ -1296,7 +1313,7 @@ namespace MWMechanics else if (type == OT_Murder) fight = fightVictim = esmStore.get().find("iFightKilling")->mValue.getInteger(); else if (type == OT_Theft) - fight = fightVictim = esmStore.get().find("fFightStealing")->mValue.getInteger(); + fight = fightVictim = esmStore.get().find("fFightStealing")->mValue.getInteger() * arg; bool reported = false; @@ -1457,7 +1474,7 @@ namespace MWMechanics if (reported) { player.getClass().getNpcStats(player).setBounty( - std::max(0, player.getClass().getNpcStats(player).getBounty() + arg)); + std::max(0, player.getClass().getNpcStats(player).getBounty() + bounty)); // If committing a crime against a faction member, expell from the faction if (!victim.isEmpty() && victim.getClass().isNpc()) @@ -1719,6 +1736,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/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 192cbdfe22..dc9d8e4061 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -25,15 +25,15 @@ 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++) @@ -62,7 +62,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,10 +197,10 @@ 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); + size_t startNode = 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 diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 0f688686cd..94242404e4 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -178,16 +178,16 @@ namespace MWMechanics // // NOTE: pos is expected to be in local coordinates, as is grid->mPoints // - static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) + static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) { assert(grid && !grid->mPoints.empty()); float distanceBetween = distanceSquared(grid->mPoints[0], pos); - int closestIndex = 0; + size_t 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++) + for (size_t counter = 1; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < distanceBetween) diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 7035c7f61c..99e5a09481 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -174,7 +174,7 @@ namespace return -1; } - void addBoundItem(const ESM::RefId& itemId, const MWWorld::Ptr& actor) + bool addBoundItem(const ESM::RefId& itemId, const MWWorld::Ptr& actor) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1); @@ -185,9 +185,6 @@ namespace MWWorld::ActionEquip action(boundPtr); action.execute(actor); - if (actor != MWMechanics::getPlayer()) - return; - MWWorld::Ptr newItem; auto it = slot >= 0 ? store.getSlot(slot) : store.end(); // Equip can fail because beast races cannot equip boots/helmets @@ -195,16 +192,21 @@ namespace newItem = *it; if (newItem.isEmpty() || boundPtr != newItem) - return; + return false; - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + if (actor == MWMechanics::getPlayer()) + { + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - player.setDrawState(MWMechanics::DrawState::Weapon); + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + player.setDrawState(MWMechanics::DrawState::Weapon); - if (prevItem != store.end()) - player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + if (prevItem != store.end()) + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } + + return true; } void removeBoundItem(const ESM::RefId& itemId, const MWWorld::Ptr& actor) @@ -517,8 +519,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; @@ -626,16 +651,19 @@ namespace MWMechanics case ESM::MagicEffect::BoundHelm: case ESM::MagicEffect::BoundBoots: case ESM::MagicEffect::BoundShield: + { if (!target.getClass().hasInventoryStore(target)) - invalid = true; - else { - const std::string& item = sBoundItemsMap.at(effect.mEffectId); - addBoundItem(ESM::RefId::stringRefId( - world->getStore().get().find(item)->mValue.getString()), - target); + invalid = true; + break; } + const std::string& item = sBoundItemsMap.at(effect.mEffectId); + const MWWorld::Store& gmst = world->getStore().get(); + const ESM::RefId itemId = ESM::RefId::stringRefId(gmst.find(item)->mValue.getString()); + if (!addBoundItem(itemId, target)) + effect.mTimeLeft = 0.f; break; + } case ESM::MagicEffect::FireDamage: case ESM::MagicEffect::ShockDamage: case ESM::MagicEffect::FrostDamage: @@ -886,7 +914,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; @@ -1004,9 +1032,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()) @@ -1078,7 +1109,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/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 022aaec262..fc7637afc1 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -274,15 +274,15 @@ namespace MWMechanics CreatureStats& stats = actor.getClass().getCreatureStats(actor); - if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude() && !godmode) - return 0; - if (spell->mData.mType == ESM::Spell::ST_Power) return stats.getSpells().canUsePower(spell) ? 100 : 0; if (godmode) return 100; + if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude()) + return 0; + if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index e236db4633..5e7c70788d 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -656,20 +656,18 @@ namespace MWPhysics auto ptr = physicActor->getPtr(); if (!ptr.getClass().isMobile(ptr)) continue; - float waterlevel = -std::numeric_limits::max(); - const MWWorld::CellStore* cell = ptr.getCell(); - if (cell->getCell()->hasWater()) - waterlevel = cell->getWaterLevel(); + const MWWorld::CellStore& cell = *ptr.getCell(); const auto& stats = ptr.getClass().getCreatureStats(ptr); const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); + float waterlevel = -std::numeric_limits::max(); bool waterCollision = false; - if (cell->getCell()->hasWater() && effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude()) + if (cell.getCell()->hasWater()) { - if (physicActor->getCollisionMode() - || !world->isUnderwater(ptr.getCell(), ptr.getRefData().getPosition().asVec3())) - waterCollision = true; + waterlevel = cell.getWaterLevel(); + if (physicActor->getCollisionMode()) + waterCollision = effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude(); } physicActor->setCanWaterWalk(waterCollision); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index f07a325f7c..d22e12ff01 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(); @@ -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 8b835a4d47..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 || 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,11 +565,11 @@ namespace MWRender mNormals = false; mPassLights = false; - std::vector attachmentsToDirty; + std::vector attachmentsToDirty; for (const auto& technique : mTechniques) { - if (!technique || !technique->isValid()) + if (!technique->isValid()) continue; if (technique->getGLSLVersion() > mGLSLVersion) @@ -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,9 +717,9 @@ namespace MWRender } PostProcessor::Status PostProcessor::enableTechnique( - std::shared_ptr technique, std::optional location) + std::shared_ptr technique, std::optional location) { - if (!technique || technique->getLocked() || (location.has_value() && location.value() < 0)) + if (technique->getLocked() || (location.has_value() && location.value() < 0)) return Status_Error; disableTechnique(technique, false); @@ -725,9 +732,9 @@ 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 || technique->getLocked()) + if (technique->getLocked()) return Status_Error; auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); @@ -741,39 +748,43 @@ namespace MWRender return Status_Toggled; } - bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const + bool PostProcessor::isTechniqueEnabled(const std::shared_ptr& technique) const { - if (!technique) - return false; - if (auto it = std::find(mTechniques.begin(), mTechniques.end(), technique); it == mTechniques.end()) return false; 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) { @@ -786,6 +797,11 @@ namespace MWRender return mTemplates.back(); } + PostProcessor::TechniqueList PostProcessor::getChain() + { + return mTechniques; + } + void PostProcessor::loadChain() { mTechniques.clear(); @@ -812,7 +828,7 @@ namespace MWRender for (const auto& technique : mTechniques) { - if (!technique || technique->getDynamic() || technique->getInternal()) + if (technique->getDynamic() || technique->getInternal()) continue; chain.push_back(technique->getName()); } @@ -823,16 +839,21 @@ namespace MWRender void PostProcessor::toggleMode() { for (auto& technique : mTemplates) + { + if (technique->getStatus() == Fx::Technique::Status::File_Not_exists) + continue; technique->compile(); + } dirtyTechniques(true); } void PostProcessor::disableDynamicShaders() { - for (auto& technique : mTechniques) - if (technique && technique->getDynamic()) - disableTechnique(technique); + auto erased = std::erase_if(mTechniques, [](const auto& technique) { return technique->getDynamic(); }); + + if (erased) + dirtyTechniques(); } int PostProcessor::renderWidth() const diff --git a/apps/openmw/mwrender/postprocessor.hpp b/apps/openmw/mwrender/postprocessor.hpp index af6eeae62b..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,10 @@ 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(); bool isEnabled() const { return mUsePostProcessing; } @@ -230,8 +233,7 @@ namespace MWRender TechniqueList mQueuedTemplates; TechniqueList mInternalTechniques; - std::unordered_map - mTechniqueFileMap; + std::unordered_set> mTechniqueFiles; RenderingManager& mRendering; osgViewer::Viewer* mViewer; @@ -261,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/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/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/globalscripts.hpp b/apps/openmw/mwscript/globalscripts.hpp index 083695b51b..47c3f1ef6c 100644 --- a/apps/openmw/mwscript/globalscripts.hpp +++ b/apps/openmw/mwscript/globalscripts.hpp @@ -64,6 +64,8 @@ namespace MWScript void removeScript(const ESM::RefId& name); + const std::unordered_map>& getScripts() const { return mScripts; } + bool isRunning(const ESM::RefId& name) const; void run(); diff --git a/apps/openmw/mwscript/guiextensions.cpp b/apps/openmw/mwscript/guiextensions.cpp index 07855f18ef..3f84362c34 100644 --- a/apps/openmw/mwscript/guiextensions.cpp +++ b/apps/openmw/mwscript/guiextensions.cpp @@ -115,6 +115,11 @@ namespace MWScript std::string_view cell = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); + // In Morrowind, using an empty string either errors out (e.g. console) or kills the game + // so it should be reasonable to interrupt the script + if (cell.empty()) + throw std::runtime_error("ShowMap substring must not be empty"); + // "Will match complete or partial cells, so ShowMap, "Vivec" will show cells Vivec and Vivec, Fred's // House as well." http://www.uesp.net/wiki/Tes3Mod:ShowMap 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 ed2ef756e6..af81fd2d3c 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -884,8 +884,8 @@ namespace MWScript return; } - const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - runtime.push(stats.getActiveSpells().isSpellActive(id)); + const auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + runtime.push(activeSpells.isSpellActive(id) || activeSpells.isEnchantmentActive(id)); } }; @@ -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,37 +1178,79 @@ 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(); - for (size_t i = 0; i < names.size(); ++i) - { - char type = world->getGlobalVariableType(names[i]); - str << std::endl << " " << names[i] << " = "; + auto& context = runtime.getContext(); + std::vector names = context.getGlobals(); + 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 << ""; } + }; + + for (const auto& name : names) + printVariable(name, world->getGlobalVariableType(name)); + + context.report(str.str()); + } + + void printGlobalScriptsVars(Interpreter::Runtime& runtime) + { + std::ostringstream str; + str << "\nGlobal Scripts:"; + + const auto& scripts = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScripts(); + + // sort for user convenience + std::map> globalScripts(scripts.begin(), scripts.end()); + + auto printVariables + = [&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 << "\n " << scptName << "->" << names[i] << " = " << values[i] << " (" << type << ")"; + } + }; + + for (const auto& [refId, script] : globalScripts) + { + // Skip dormant global scripts + 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 << "\n No variables in script " << scptName; + else + { + printVariables(scptName, complocals.get('s'), locals.mShorts, "short"); + printVariables(scptName, complocals.get('l'), locals.mLongs, "long"); + printVariables(scptName, complocals.get('f'), locals.mFloats, "float"); + } } runtime.getContext().report(str.str()); @@ -1233,6 +1266,7 @@ namespace MWScript { // No reference, no problem. printGlobalVars(runtime); + printGlobalScriptsVars(runtime); } } }; diff --git a/apps/openmw/mwsound/efx-presets.h b/apps/openmw/mwsound/efxpresets.h similarity index 99% rename from apps/openmw/mwsound/efx-presets.h rename to apps/openmw/mwsound/efxpresets.h index a9662936d5..a36c41db03 100644 --- a/apps/openmw/mwsound/efx-presets.h +++ b/apps/openmw/mwsound/efxpresets.h @@ -1,7 +1,7 @@ /* Reverb presets for EFX */ -#ifndef EFX_PRESETS_H -#define EFX_PRESETS_H +#ifndef GAME_SOUND_EFXPRESETS_H +#define GAME_SOUND_EFXPRESETS_H #ifndef EFXEAXREVERBPROPERTIES_DEFINED #define EFXEAXREVERBPROPERTIES_DEFINED @@ -852,4 +852,4 @@ typedef struct 0.1900f, 0.9920f, 5000.0000f, 250.0000f, 0.0000f, 0x0 \ } -#endif /* EFX_PRESETS_H */ +#endif diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpegdecoder.cpp similarity index 93% rename from apps/openmw/mwsound/ffmpeg_decoder.cpp rename to apps/openmw/mwsound/ffmpegdecoder.cpp index 54fd126c41..5a0f336a93 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpegdecoder.cpp @@ -1,4 +1,4 @@ -#include "ffmpeg_decoder.hpp" +#include "ffmpegdecoder.hpp" #include #include @@ -44,11 +44,11 @@ namespace MWSound av_frame_free(&ptr); } - int FFmpeg_Decoder::readPacket(void* user_data, uint8_t* buf, int buf_size) + int FFmpegDecoder::readPacket(void* user_data, uint8_t* buf, int buf_size) { try { - std::istream& stream = *static_cast(user_data)->mDataStream; + std::istream& stream = *static_cast(user_data)->mDataStream; stream.clear(); stream.read((char*)buf, buf_size); std::streamsize count = stream.gcount(); @@ -65,18 +65,18 @@ namespace MWSound } #if OPENMW_FFMPEG_CONST_WRITEPACKET - int FFmpeg_Decoder::writePacket(void*, const uint8_t*, int) + int FFmpegDecoder::writePacket(void*, const uint8_t*, int) #else - int FFmpeg_Decoder::writePacket(void*, uint8_t*, int) + int FFmpegDecoder::writePacket(void*, uint8_t*, int) #endif { Log(Debug::Error) << "can't write to read-only stream"; return -1; } - int64_t FFmpeg_Decoder::seek(void* user_data, int64_t offset, int whence) + int64_t FFmpegDecoder::seek(void* user_data, int64_t offset, int whence) { - std::istream& stream = *static_cast(user_data)->mDataStream; + std::istream& stream = *static_cast(user_data)->mDataStream; whence &= ~AVSEEK_FORCE; @@ -106,7 +106,7 @@ namespace MWSound /* Used by getAV*Data to search for more compressed data, and buffer it in the * correct stream. It won't buffer data for streams that the app doesn't have a * handle for. */ - bool FFmpeg_Decoder::getNextPacket() + bool FFmpegDecoder::getNextPacket() { if (!mStream) return false; @@ -129,7 +129,7 @@ namespace MWSound return false; } - bool FFmpeg_Decoder::getAVAudioData() + bool FFmpegDecoder::getAVAudioData() { bool got_frame = false; @@ -192,7 +192,7 @@ namespace MWSound return true; } - size_t FFmpeg_Decoder::readAVAudioData(void* data, size_t length) + size_t FFmpegDecoder::readAVAudioData(void* data, size_t length) { size_t dec = 0; @@ -227,7 +227,7 @@ namespace MWSound return dec; } - void FFmpeg_Decoder::open(VFS::Path::NormalizedView fname) + void FFmpegDecoder::open(VFS::Path::NormalizedView fname) { close(); mDataStream = mResourceMgr->get(fname); @@ -317,7 +317,7 @@ namespace MWSound mStream = stream; } - void FFmpeg_Decoder::close() + void FFmpegDecoder::close() { mStream = nullptr; mCodecCtx.reset(); @@ -332,7 +332,7 @@ namespace MWSound mDataStream.reset(); } - std::string FFmpeg_Decoder::getName() + std::string FFmpegDecoder::getName() { // In the FFMpeg 4.0 a "filename" field was replaced by "url" #if LIBAVCODEC_VERSION_INT < 3805796 @@ -342,7 +342,7 @@ namespace MWSound #endif } - void FFmpeg_Decoder::getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) + void FFmpegDecoder::getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) { if (!mStream) throw std::runtime_error("No audio stream info"); @@ -459,7 +459,7 @@ namespace MWSound } } - size_t FFmpeg_Decoder::read(char* buffer, size_t bytes) + size_t FFmpegDecoder::read(char* buffer, size_t bytes) { if (!mStream) { @@ -469,7 +469,7 @@ namespace MWSound return readAVAudioData(buffer, bytes); } - void FFmpeg_Decoder::readAll(std::vector& output) + void FFmpegDecoder::readAll(std::vector& output) { if (!mStream) { @@ -490,7 +490,7 @@ namespace MWSound } } - size_t FFmpeg_Decoder::getSampleOffset() + size_t FFmpegDecoder::getSampleOffset() { #if OPENMW_FFMPEG_5_OR_GREATER std::size_t delay = (mFrameSize - mFramePos) / mOutputChannelLayout.nb_channels @@ -501,8 +501,8 @@ namespace MWSound return static_cast(mNextPts * mCodecCtx->sample_rate) - delay; } - FFmpeg_Decoder::FFmpeg_Decoder(const VFS::Manager* vfs) - : Sound_Decoder(vfs) + FFmpegDecoder::FFmpegDecoder(const VFS::Manager* vfs) + : SoundDecoder(vfs) , mStream(nullptr) , mFrameSize(0) , mFramePos(0) @@ -534,7 +534,7 @@ namespace MWSound } } - FFmpeg_Decoder::~FFmpeg_Decoder() + FFmpegDecoder::~FFmpegDecoder() { close(); } diff --git a/apps/openmw/mwsound/ffmpeg_decoder.hpp b/apps/openmw/mwsound/ffmpegdecoder.hpp similarity index 89% rename from apps/openmw/mwsound/ffmpeg_decoder.hpp rename to apps/openmw/mwsound/ffmpegdecoder.hpp index e67b8efbf3..c71b935bc4 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.hpp +++ b/apps/openmw/mwsound/ffmpegdecoder.hpp @@ -1,5 +1,5 @@ -#ifndef GAME_SOUND_FFMPEG_DECODER_H -#define GAME_SOUND_FFMPEG_DECODER_H +#ifndef GAME_SOUND_FFMPEGDECODER_H +#define GAME_SOUND_FFMPEGDECODER_H #include @@ -31,7 +31,7 @@ extern "C" #include -#include "sound_decoder.hpp" +#include "sounddecoder.hpp" namespace MWSound { @@ -63,7 +63,7 @@ namespace MWSound using AVFramePtr = std::unique_ptr; - class FFmpeg_Decoder final : public Sound_Decoder + class FFmpegDecoder final : public SoundDecoder { AVIOContextPtr mIoCtx; AVFormatContextPtr mFormatCtx; @@ -114,13 +114,13 @@ namespace MWSound void readAll(std::vector& output) override; size_t getSampleOffset() override; - FFmpeg_Decoder& operator=(const FFmpeg_Decoder& rhs); - FFmpeg_Decoder(const FFmpeg_Decoder& rhs); + FFmpegDecoder& operator=(const FFmpegDecoder& rhs); + FFmpegDecoder(const FFmpegDecoder& rhs); public: - explicit FFmpeg_Decoder(const VFS::Manager* vfs); + explicit FFmpegDecoder(const VFS::Manager* vfs); - virtual ~FFmpeg_Decoder(); + virtual ~FFmpegDecoder(); friend class SoundManager; }; diff --git a/apps/openmw/mwsound/loudness.hpp b/apps/openmw/mwsound/loudness.hpp index 1800ec246f..edc201f38a 100644 --- a/apps/openmw/mwsound/loudness.hpp +++ b/apps/openmw/mwsound/loudness.hpp @@ -4,7 +4,7 @@ #include #include -#include "sound_decoder.hpp" +#include "sounddecoder.hpp" namespace MWSound { diff --git a/apps/openmw/mwsound/movieaudiofactory.cpp b/apps/openmw/mwsound/movieaudiofactory.cpp index 962086701a..61992bc0d5 100644 --- a/apps/openmw/mwsound/movieaudiofactory.cpp +++ b/apps/openmw/mwsound/movieaudiofactory.cpp @@ -7,17 +7,17 @@ #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" -#include "sound_decoder.hpp" +#include "sounddecoder.hpp" namespace MWSound { class MovieAudioDecoder; - class MWSoundDecoderBridge final : public Sound_Decoder + class MWSoundDecoderBridge final : public SoundDecoder { public: MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder) - : Sound_Decoder(nullptr) + : SoundDecoder(nullptr) , mDecoder(decoder) { } diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openaloutput.cpp similarity index 95% rename from apps/openmw/mwsound/openal_output.cpp rename to apps/openmw/mwsound/openaloutput.cpp index f829ea458a..60c9e5f3ea 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openaloutput.cpp @@ -17,14 +17,13 @@ #include #include +#include "efxpresets.h" #include "loudness.hpp" -#include "openal_output.hpp" +#include "openaloutput.hpp" #include "sound.hpp" -#include "sound_decoder.hpp" +#include "sounddecoder.hpp" #include "soundmanagerimp.hpp" -#include "efx-presets.h" - #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif @@ -35,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) { @@ -301,7 +300,7 @@ namespace MWSound OpenAL_SoundStream(const OpenAL_SoundStream& rhs); OpenAL_SoundStream& operator=(const OpenAL_SoundStream& rhs); - friend class OpenAL_Output; + friend class OpenALOutput; public: OpenAL_SoundStream(ALuint src, DecoderPtr decoder); @@ -323,7 +322,7 @@ namespace MWSound // // A background streaming thread (keeps active streams processed) // - struct OpenAL_Output::StreamThread + struct OpenALOutput::StreamThread { std::vector mStreams; @@ -393,13 +392,13 @@ namespace MWSound StreamThread& operator=(const StreamThread& rhs) = delete; }; - class OpenAL_Output::DefaultDeviceThread + class OpenALOutput::DefaultDeviceThread { public: std::basic_string mCurrentName; private: - OpenAL_Output& mOutput; + OpenALOutput& mOutput; std::atomic mQuitNow; std::mutex mMutex; @@ -433,7 +432,7 @@ namespace MWSound } public: - DefaultDeviceThread(OpenAL_Output& output, std::basic_string_view name = {}) + DefaultDeviceThread(OpenALOutput& output, std::basic_string_view name = {}) : mCurrentName(name) , mOutput(output) , mQuitNow(false) @@ -655,7 +654,7 @@ namespace MWSound // // An OpenAL output device // - std::vector OpenAL_Output::enumerate() + std::vector OpenALOutput::enumerate() { std::vector devlist; const ALCchar* devnames; @@ -672,14 +671,14 @@ namespace MWSound return devlist; } - void OpenAL_Output::eventCallback( + void OpenALOutput::eventCallback( ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam) { if (eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT) - static_cast(userParam)->onDisconnect(); + static_cast(userParam)->onDisconnect(); } - void OpenAL_Output::onDisconnect() + void OpenALOutput::onDisconnect() { if (!mInitialized || !alcReopenDeviceSOFT) return; @@ -702,7 +701,7 @@ namespace MWSound } } - bool OpenAL_Output::init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) + bool OpenALOutput::init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) { deinit(); std::lock_guard lock(mReopenMutex); @@ -802,7 +801,7 @@ namespace MWSound { static const std::array events{ { AL_EVENT_TYPE_DISCONNECTED_SOFT } }; alEventControlSOFT(events.size(), events.data(), AL_TRUE); - alEventCallbackSOFT(&OpenAL_Output::eventCallback, this); + alEventCallbackSOFT(&OpenALOutput::eventCallback, this); } else Log(Debug::Warning) << "Cannot detect audio device changes"; @@ -970,7 +969,7 @@ namespace MWSound return true; } - void OpenAL_Output::deinit() + void OpenALOutput::deinit() { mStreamThread->removeAll(); mDefaultDeviceThread.reset(); @@ -1006,7 +1005,7 @@ namespace MWSound mInitialized = false; } - std::vector OpenAL_Output::enumerateHrtf() + std::vector OpenALOutput::enumerateHrtf() { std::vector ret; @@ -1028,7 +1027,7 @@ namespace MWSound return ret; } - std::pair OpenAL_Output::loadSound(VFS::Path::NormalizedView fname) + std::pair OpenALOutput::loadSound(VFS::Path::NormalizedView fname) { getALError(); @@ -1076,7 +1075,7 @@ namespace MWSound return std::make_pair(MAKE_PTRID(buf), size); } - size_t OpenAL_Output::unloadSound(Sound_Handle data) + size_t OpenALOutput::unloadSound(Sound_Handle data) { ALuint buffer = GET_PTRID(data); if (!buffer) @@ -1105,7 +1104,7 @@ namespace MWSound return size; } - void OpenAL_Output::initCommon2D( + void OpenALOutput::initCommon2D( ALuint source, const osg::Vec3f& pos, ALfloat gain, ALfloat pitch, bool loop, bool useenv) { alSourcef(source, AL_REFERENCE_DISTANCE, 1.0f); @@ -1143,7 +1142,7 @@ namespace MWSound alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } - void OpenAL_Output::initCommon3D(ALuint source, const osg::Vec3f& pos, ALfloat mindist, ALfloat maxdist, + void OpenALOutput::initCommon3D(ALuint source, const osg::Vec3f& pos, ALfloat mindist, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool loop, bool useenv) { alSourcef(source, AL_REFERENCE_DISTANCE, mindist); @@ -1183,7 +1182,7 @@ namespace MWSound alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } - void OpenAL_Output::updateCommon( + void OpenALOutput::updateCommon( ALuint source, const osg::Vec3f& pos, ALfloat maxdist, ALfloat gain, ALfloat pitch, bool useenv) { if (useenv && mListenerEnv == Env_Underwater && !mWaterFilter) @@ -1199,7 +1198,7 @@ namespace MWSound alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); } - bool OpenAL_Output::playSound(Sound* sound, Sound_Handle data, float offset) + bool OpenALOutput::playSound(Sound* sound, Sound_Handle data, float offset) { ALuint source; @@ -1238,7 +1237,7 @@ namespace MWSound return true; } - bool OpenAL_Output::playSound3D(Sound* sound, Sound_Handle data, float offset) + bool OpenALOutput::playSound3D(Sound* sound, Sound_Handle data, float offset) { ALuint source; @@ -1277,7 +1276,7 @@ namespace MWSound return true; } - void OpenAL_Output::finishSound(Sound* sound) + void OpenALOutput::finishSound(Sound* sound) { if (!sound->mHandle) return; @@ -1294,7 +1293,7 @@ namespace MWSound mActiveSounds.erase(std::find(mActiveSounds.begin(), mActiveSounds.end(), sound)); } - bool OpenAL_Output::isSoundPlaying(Sound* sound) + bool OpenALOutput::isSoundPlaying(Sound* sound) { if (!sound->mHandle) return false; @@ -1307,7 +1306,7 @@ namespace MWSound return state == AL_PLAYING || state == AL_PAUSED; } - void OpenAL_Output::updateSound(Sound* sound) + void OpenALOutput::updateSound(Sound* sound) { if (!sound->mHandle) return; @@ -1318,7 +1317,7 @@ namespace MWSound getALError(); } - bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData) + bool OpenALOutput::streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData) { if (mFreeSources.empty()) { @@ -1349,7 +1348,7 @@ namespace MWSound return true; } - bool OpenAL_Output::streamSound3D(DecoderPtr decoder, Stream* sound, bool getLoudnessData) + bool OpenALOutput::streamSound3D(DecoderPtr decoder, Stream* sound, bool getLoudnessData) { if (mFreeSources.empty()) { @@ -1380,7 +1379,7 @@ namespace MWSound return true; } - void OpenAL_Output::finishStream(Stream* sound) + void OpenALOutput::finishStream(Stream* sound) { if (!sound->mHandle) return; @@ -1402,7 +1401,7 @@ namespace MWSound delete stream; } - double OpenAL_Output::getStreamDelay(Stream* sound) + double OpenALOutput::getStreamDelay(Stream* sound) { if (!sound->mHandle) return 0.0; @@ -1410,7 +1409,7 @@ namespace MWSound return stream->getStreamDelay(); } - double OpenAL_Output::getStreamOffset(Stream* sound) + double OpenALOutput::getStreamOffset(Stream* sound) { if (!sound->mHandle) return 0.0; @@ -1419,7 +1418,7 @@ namespace MWSound return stream->getStreamOffset(); } - float OpenAL_Output::getStreamLoudness(Stream* sound) + float OpenALOutput::getStreamLoudness(Stream* sound) { if (!sound->mHandle) return 0.0; @@ -1428,7 +1427,7 @@ namespace MWSound return stream->getCurrentLoudness(); } - bool OpenAL_Output::isStreamPlaying(Stream* sound) + bool OpenALOutput::isStreamPlaying(Stream* sound) { if (!sound->mHandle) return false; @@ -1437,7 +1436,7 @@ namespace MWSound return stream->isPlaying(); } - void OpenAL_Output::updateStream(Stream* sound) + void OpenALOutput::updateStream(Stream* sound) { if (!sound->mHandle) return; @@ -1449,17 +1448,17 @@ namespace MWSound getALError(); } - void OpenAL_Output::startUpdate() + void OpenALOutput::startUpdate() { alcSuspendContext(alcGetCurrentContext()); } - void OpenAL_Output::finishUpdate() + void OpenALOutput::finishUpdate() { alcProcessContext(alcGetCurrentContext()); } - void OpenAL_Output::updateListener( + void OpenALOutput::updateListener( const osg::Vec3f& pos, const osg::Vec3f& atdir, const osg::Vec3f& updir, Environment env) { if (mContext) @@ -1501,7 +1500,7 @@ namespace MWSound mListenerEnv = env; } - void OpenAL_Output::pauseSounds(int types) + void OpenALOutput::pauseSounds(int types) { std::vector sources; for (Sound* sound : mActiveSounds) @@ -1524,7 +1523,7 @@ namespace MWSound } } - void OpenAL_Output::pauseActiveDevice() + void OpenALOutput::pauseActiveDevice() { if (mDevice == nullptr) return; @@ -1540,7 +1539,7 @@ namespace MWSound alListenerf(AL_GAIN, 0.0f); } - void OpenAL_Output::resumeActiveDevice() + void OpenALOutput::resumeActiveDevice() { if (mDevice == nullptr) return; @@ -1556,7 +1555,7 @@ namespace MWSound alListenerf(AL_GAIN, 1.0f); } - void OpenAL_Output::resumeSounds(int types) + void OpenALOutput::resumeSounds(int types) { std::vector sources; for (Sound* sound : mActiveSounds) @@ -1579,8 +1578,8 @@ namespace MWSound } } - OpenAL_Output::OpenAL_Output(SoundManager& mgr) - : Sound_Output(mgr) + OpenALOutput::OpenALOutput(SoundManager& mgr) + : SoundOutput(mgr) , mDevice(nullptr) , mContext(nullptr) , mListenerPos(0.0f, 0.0f, 0.0f) @@ -1593,12 +1592,12 @@ namespace MWSound { } - OpenAL_Output::~OpenAL_Output() + OpenALOutput::~OpenALOutput() { - OpenAL_Output::deinit(); + OpenALOutput::deinit(); } - float OpenAL_Output::getTimeScaledPitch(SoundBase* sound) + float OpenALOutput::getTimeScaledPitch(SoundBase* sound) { const bool shouldScale = !(sound->mParams.mFlags & PlayMode::NoScaling); return shouldScale ? sound->getPitch() * mManager.getSimulationTimeScale() : sound->getPitch(); diff --git a/apps/openmw/mwsound/openal_output.hpp b/apps/openmw/mwsound/openaloutput.hpp similarity index 91% rename from apps/openmw/mwsound/openal_output.hpp rename to apps/openmw/mwsound/openaloutput.hpp index b419038eab..4e96dd1627 100644 --- a/apps/openmw/mwsound/openal_output.hpp +++ b/apps/openmw/mwsound/openaloutput.hpp @@ -1,5 +1,5 @@ -#ifndef GAME_SOUND_OPENAL_OUTPUT_H -#define GAME_SOUND_OPENAL_OUTPUT_H +#ifndef GAME_SOUND_OPENALOUTPUT_H +#define GAME_SOUND_OPENALOUTPUT_H #include #include @@ -13,7 +13,7 @@ #include "alc.h" #include "alext.h" -#include "sound_output.hpp" +#include "soundoutput.hpp" namespace MWSound { @@ -22,7 +22,7 @@ namespace MWSound class Sound; class Stream; - class OpenAL_Output : public Sound_Output + class OpenALOutput : public SoundOutput { ALCdevice* mDevice; ALCcontext* mContext; @@ -72,8 +72,8 @@ namespace MWSound float getTimeScaledPitch(SoundBase* sound); - OpenAL_Output& operator=(const OpenAL_Output& rhs); - OpenAL_Output(const OpenAL_Output& rhs); + OpenALOutput& operator=(const OpenALOutput& rhs); + OpenALOutput(const OpenALOutput& rhs); static void eventCallback( ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam); @@ -117,8 +117,8 @@ namespace MWSound void pauseActiveDevice() override; void resumeActiveDevice() override; - OpenAL_Output(SoundManager& mgr); - virtual ~OpenAL_Output(); + OpenALOutput(SoundManager& mgr); + virtual ~OpenALOutput(); }; } diff --git a/apps/openmw/mwsound/sound.hpp b/apps/openmw/mwsound/sound.hpp index 48e8975340..5160b2934f 100644 --- a/apps/openmw/mwsound/sound.hpp +++ b/apps/openmw/mwsound/sound.hpp @@ -3,7 +3,7 @@ #include -#include "sound_output.hpp" +#include "soundoutput.hpp" namespace MWSound { @@ -53,7 +53,7 @@ namespace MWSound protected: Sound_Instance mHandle = nullptr; - friend class OpenAL_Output; + friend class OpenALOutput; public: void setPosition(const osg::Vec3f& pos) { mParams.mPos = pos; } diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/soundbuffer.cpp similarity index 66% rename from apps/openmw/mwsound/sound_buffer.cpp rename to apps/openmw/mwsound/soundbuffer.cpp index f28b268df2..0c10ba5552 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/soundbuffer.cpp @@ -1,11 +1,14 @@ -#include "sound_buffer.hpp" +#include "soundbuffer.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include #include +#include +#include #include +#include #include #include @@ -35,7 +38,7 @@ namespace MWSound } } - SoundBufferPool::SoundBufferPool(Sound_Output& output) + SoundBufferPool::SoundBufferPool(SoundOutput& output) : mOutput(&output) , mBufferCacheMax(Settings::sound().mBufferCacheMax * 1024 * 1024) , mBufferCacheMin( @@ -48,31 +51,31 @@ namespace MWSound clear(); } - Sound_Buffer* SoundBufferPool::lookup(const ESM::RefId& soundId) const + SoundBuffer* SoundBufferPool::lookup(const ESM::RefId& soundId) const { const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) { - Sound_Buffer* sfx = it->second; + SoundBuffer* sfx = it->second; if (sfx->getHandle() != nullptr) return sfx; } return nullptr; } - Sound_Buffer* SoundBufferPool::lookup(std::string_view fileName) const + SoundBuffer* SoundBufferPool::lookup(std::string_view fileName) const { const auto it = mBufferFileNameMap.find(std::string(fileName)); if (it != mBufferFileNameMap.end()) { - Sound_Buffer* sfx = it->second; + SoundBuffer* sfx = it->second; if (sfx->getHandle() != nullptr) return sfx; } return nullptr; } - Sound_Buffer* SoundBufferPool::loadSfx(Sound_Buffer* sfx) + SoundBuffer* SoundBufferPool::loadSfx(SoundBuffer* sfx) { if (sfx->getHandle() != nullptr) return sfx; @@ -95,15 +98,20 @@ namespace MWSound return sfx; } - Sound_Buffer* SoundBufferPool::load(const ESM::RefId& soundId) + SoundBuffer* SoundBufferPool::load(const ESM::RefId& soundId) { 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); } - Sound_Buffer* sfx; + SoundBuffer* sfx; const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) sfx = it->second; @@ -118,9 +126,9 @@ namespace MWSound return loadSfx(sfx); } - Sound_Buffer* SoundBufferPool::load(std::string_view fileName) + SoundBuffer* SoundBufferPool::load(std::string_view fileName) { - Sound_Buffer* sfx; + SoundBuffer* sfx; const auto it = mBufferFileNameMap.find(std::string(fileName)); if (it != mBufferFileNameMap.end()) sfx = it->second; @@ -146,7 +154,7 @@ namespace MWSound mUnusedBuffers.clear(); } - Sound_Buffer* SoundBufferPool::insertSound(std::string_view fileName) + SoundBuffer* SoundBufferPool::insertSound(std::string_view fileName) { static const AudioParams audioParams = makeAudioParams(MWBase::Environment::get().getESMStore()->get()); @@ -158,13 +166,13 @@ namespace MWSound min = std::max(min, 1.0f); max = std::max(min, max); - Sound_Buffer& sfx = mSoundBuffers.emplace_back(fileName, volume, min, max); + SoundBuffer& sfx = mSoundBuffers.emplace_back(fileName, volume, min, max); mBufferFileNameMap.emplace(fileName, &sfx); return &sfx; } - Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM::Sound& sound) + SoundBuffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM::Sound& sound) { static const AudioParams audioParams = makeAudioParams(MWBase::Environment::get().getESMStore()->get()); @@ -183,18 +191,40 @@ namespace MWSound min = std::max(min, 1.0f); max = std::max(min, max); - Sound_Buffer& sfx = mSoundBuffers.emplace_back( + SoundBuffer& sfx = mSoundBuffers.emplace_back( Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(sound.mSound)), volume, min, max); mBufferNameMap.emplace(soundId, &sfx); 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) { - Sound_Buffer* const unused = mUnusedBuffers.back(); + SoundBuffer* const unused = mUnusedBuffers.back(); mBufferCacheSize -= mOutput->unloadSound(unused->getHandle()); unused->mHandle = nullptr; diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/soundbuffer.hpp similarity index 63% rename from apps/openmw/mwsound/sound_buffer.hpp rename to apps/openmw/mwsound/soundbuffer.hpp index 7de6dab9ae..493577a9c3 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/soundbuffer.hpp @@ -1,19 +1,26 @@ -#ifndef GAME_SOUND_SOUND_BUFFER_H -#define GAME_SOUND_SOUND_BUFFER_H +#ifndef GAME_SOUND_SOUNDBUFFER_H +#define GAME_SOUND_SOUNDBUFFER_H #include #include #include #include -#include "sound_output.hpp" #include +#include "soundoutput.hpp" + namespace ESM { struct Sound; } +namespace ESM4 +{ + struct Sound; + struct SoundReference; +} + namespace VFS { class Manager; @@ -23,11 +30,11 @@ namespace MWSound { class SoundBufferPool; - class Sound_Buffer + class SoundBuffer { public: template - Sound_Buffer(T&& resname, float volume, float mindist, float maxdist) + SoundBuffer(T&& resname, float volume, float mindist, float maxdist) : mResourceName(std::forward(resname)) , mVolume(volume) , mMinDist(mindist) @@ -59,7 +66,7 @@ namespace MWSound class SoundBufferPool { public: - SoundBufferPool(Sound_Output& output); + SoundBufferPool(SoundOutput& output); SoundBufferPool(const SoundBufferPool&) = delete; @@ -67,20 +74,20 @@ namespace MWSound /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange) - Sound_Buffer* lookup(const ESM::RefId& soundId) const; + SoundBuffer* lookup(const ESM::RefId& soundId) const; /// Lookup a sound by file name for its sound data (resource name, local volume, /// minRange, and maxRange) - Sound_Buffer* lookup(std::string_view fileName) const; + SoundBuffer* lookup(std::string_view fileName) const; /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange), and ensure it's ready for use. - Sound_Buffer* load(const ESM::RefId& soundId); + SoundBuffer* load(const ESM::RefId& soundId); // Lookup for a sound by file name, and ensure it's ready for use. - Sound_Buffer* load(std::string_view fileName); + SoundBuffer* load(std::string_view fileName); - void use(Sound_Buffer& sfx) + void use(SoundBuffer& sfx) { if (sfx.mUses++ == 0) { @@ -90,7 +97,7 @@ namespace MWSound } } - void release(Sound_Buffer& sfx) + void release(SoundBuffer& sfx) { if (--sfx.mUses == 0) mUnusedBuffers.push_front(&sfx); @@ -99,23 +106,25 @@ namespace MWSound void clear(); private: - Sound_Buffer* loadSfx(Sound_Buffer* sfx); + SoundBuffer* loadSfx(SoundBuffer* sfx); - Sound_Output* mOutput; - std::deque mSoundBuffers; - std::unordered_map mBufferNameMap; - std::unordered_map mBufferFileNameMap; + SoundOutput* mOutput; + std::deque mSoundBuffers; + std::unordered_map mBufferNameMap; + std::unordered_map mBufferFileNameMap; std::size_t mBufferCacheMax; std::size_t mBufferCacheMin; std::size_t mBufferCacheSize = 0; // NOTE: unused buffers are stored in front-newest order. - std::deque mUnusedBuffers; + std::deque mUnusedBuffers; - inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); - inline Sound_Buffer* 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(); }; } -#endif /* GAME_SOUND_SOUND_BUFFER_H */ +#endif /* GAME_SOUND_SOUNDBUFFER_H */ diff --git a/apps/openmw/mwsound/sound_decoder.hpp b/apps/openmw/mwsound/sounddecoder.hpp similarity index 80% rename from apps/openmw/mwsound/sound_decoder.hpp rename to apps/openmw/mwsound/sounddecoder.hpp index 17f9d28909..675fad4197 100644 --- a/apps/openmw/mwsound/sound_decoder.hpp +++ b/apps/openmw/mwsound/sounddecoder.hpp @@ -1,5 +1,5 @@ -#ifndef GAME_SOUND_SOUND_DECODER_H -#define GAME_SOUND_SOUND_DECODER_H +#ifndef GAME_SOUND_SOUNDDECODER_H +#define GAME_SOUND_SOUNDDECODER_H #include @@ -34,7 +34,7 @@ namespace MWSound size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type); size_t bytesToFrames(size_t bytes, ChannelConfig config, SampleType type); - struct Sound_Decoder + struct SoundDecoder { const VFS::Manager* mResourceMgr; @@ -48,15 +48,15 @@ namespace MWSound virtual void readAll(std::vector& output); virtual size_t getSampleOffset() = 0; - Sound_Decoder(const VFS::Manager* resourceMgr) + SoundDecoder(const VFS::Manager* resourceMgr) : mResourceMgr(resourceMgr) { } - virtual ~Sound_Decoder() {} + virtual ~SoundDecoder() {} private: - Sound_Decoder(const Sound_Decoder& rhs); - Sound_Decoder& operator=(const Sound_Decoder& rhs); + SoundDecoder(const SoundDecoder& rhs); + SoundDecoder& operator=(const SoundDecoder& rhs); }; } diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 1d9a6d457c..66bdfbdbfa 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -26,12 +26,12 @@ #include "../mwmechanics/actorutil.hpp" #include "constants.hpp" -#include "ffmpeg_decoder.hpp" -#include "openal_output.hpp" +#include "ffmpegdecoder.hpp" +#include "openaloutput.hpp" #include "sound.hpp" -#include "sound_buffer.hpp" -#include "sound_decoder.hpp" -#include "sound_output.hpp" +#include "soundbuffer.hpp" +#include "sounddecoder.hpp" +#include "soundoutput.hpp" namespace MWSound { @@ -57,7 +57,7 @@ namespace MWSound return settings; } - float initialFadeVolume(float squaredDist, Sound_Buffer* sfx, Type type, PlayMode mode) + float initialFadeVolume(float squaredDist, SoundBuffer* sfx, Type type, PlayMode mode) { // If a sound is farther away than its maximum distance, start playing it with a zero fade volume. // It can still become audible once the player moves closer. @@ -111,7 +111,7 @@ namespace MWSound SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound) : mVFS(vfs) - , mOutput(std::make_unique(*this)) + , mOutput(std::make_unique(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) , mSoundBuffers(*mOutput) , mMusicType(MWSound::MusicType::Normal) @@ -169,7 +169,7 @@ namespace MWSound // Return a new decoder instance, used as needed by the output implementations DecoderPtr SoundManager::getDecoder() { - return std::make_shared(mVFS); + return std::make_shared(mVFS); } DecoderPtr SoundManager::loadVoice(VFS::Path::NormalizedView voicefile) @@ -459,7 +459,7 @@ namespace MWSound return false; } - Sound* SoundManager::playSound(Sound_Buffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) + Sound* SoundManager::playSound(SoundBuffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) { if (!mOutput->isInitialized()) return nullptr; @@ -495,7 +495,7 @@ namespace MWSound if (!mVFS->exists(normalizedName)) return nullptr; - Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + SoundBuffer* sfx = mSoundBuffers.load(normalizedName); if (!sfx) return nullptr; @@ -508,14 +508,14 @@ namespace MWSound if (!mOutput->isInitialized()) return nullptr; - Sound_Buffer* sfx = mSoundBuffers.load(soundId); + SoundBuffer* sfx = mSoundBuffers.load(soundId); if (!sfx) return nullptr; return playSound(sfx, volume, pitch, type, mode, offset); } - Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, Sound_Buffer* sfx, float volume, float pitch, + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, SoundBuffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) { if (!mOutput->isInitialized()) @@ -576,7 +576,7 @@ namespace MWSound return nullptr; // Look up the sound in the ESM data - Sound_Buffer* sfx = mSoundBuffers.load(soundId); + SoundBuffer* sfx = mSoundBuffers.load(soundId); if (!sfx) return nullptr; @@ -594,7 +594,7 @@ namespace MWSound if (!mVFS->exists(normalizedName)) return nullptr; - Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + SoundBuffer* sfx = mSoundBuffers.load(normalizedName); if (!sfx) return nullptr; @@ -608,7 +608,7 @@ namespace MWSound return nullptr; // Look up the sound in the ESM data - Sound_Buffer* sfx = mSoundBuffers.load(soundId); + SoundBuffer* sfx = mSoundBuffers.load(soundId); if (!sfx) return nullptr; @@ -642,7 +642,7 @@ namespace MWSound mOutput->finishSound(sound); } - void SoundManager::stopSound(Sound_Buffer* sfx, const MWWorld::ConstPtr& ptr) + void SoundManager::stopSound(SoundBuffer* sfx, const MWWorld::ConstPtr& ptr) { SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) @@ -660,7 +660,7 @@ namespace MWSound if (!mOutput->isInitialized()) return; - Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); + SoundBuffer* sfx = mSoundBuffers.lookup(soundId); if (!sfx) return; @@ -673,7 +673,7 @@ namespace MWSound return; std::string normalizedName = VFS::Path::normalizeFilename(fileName); - Sound_Buffer* sfx = mSoundBuffers.lookup(normalizedName); + SoundBuffer* sfx = mSoundBuffers.lookup(normalizedName); if (!sfx) return; @@ -725,7 +725,7 @@ namespace MWSound SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { - Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); + SoundBuffer* sfx = mSoundBuffers.lookup(soundId); if (sfx == nullptr) return; for (SoundBufferRefPair& sndbuf : snditer->second.mList) @@ -743,7 +743,7 @@ namespace MWSound SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { - Sound_Buffer* sfx = mSoundBuffers.lookup(normalizedName); + SoundBuffer* sfx = mSoundBuffers.lookup(normalizedName); if (!sfx) return false; @@ -761,7 +761,7 @@ namespace MWSound SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { - Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); + SoundBuffer* sfx = mSoundBuffers.lookup(soundId); if (!sfx) return false; @@ -846,7 +846,7 @@ namespace MWSound const auto update = mWaterSoundUpdater.update(player, *world); WaterSoundAction action; - Sound_Buffer* sfx; + SoundBuffer* sfx; std::tie(action, sfx) = getWaterSoundAction(update, curcell); switch (action) @@ -870,7 +870,7 @@ namespace MWSound mLastCell = curcell; } - std::pair SoundManager::getWaterSoundAction( + std::pair SoundManager::getWaterSoundAction( const WaterSoundUpdate& update, const MWWorld::Cell* cell) const { if (mNearWaterSound) @@ -880,7 +880,7 @@ namespace MWSound bool soundIdChanged = false; - Sound_Buffer* sfx = mSoundBuffers.lookup(update.mId); + SoundBuffer* sfx = mSoundBuffers.lookup(update.mId); if (mLastCell != cell) { const auto snditer = mActiveSounds.find(nullptr); @@ -1168,7 +1168,7 @@ namespace MWSound // Default readAll implementation, for decoders that can't do anything // better - void Sound_Decoder::readAll(std::vector& output) + void SoundDecoder::readAll(std::vector& output) { size_t total = output.size(); size_t got; diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index a5e5b2c45f..8fc7e6701f 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -15,7 +15,7 @@ #include "../mwbase/soundmanager.hpp" #include "regionsoundselector.hpp" -#include "sound_buffer.hpp" +#include "soundbuffer.hpp" #include "type.hpp" #include "watersoundupdater.hpp" @@ -37,8 +37,8 @@ namespace MWWorld namespace MWSound { - class Sound_Output; - struct Sound_Decoder; + class SoundOutput; + struct SoundDecoder; class SoundBase; class Sound; class Stream; @@ -50,7 +50,7 @@ namespace MWSound { const VFS::Manager* mVFS; - std::unique_ptr mOutput; + std::unique_ptr mOutput; WaterSoundUpdater mWaterSoundUpdater; @@ -60,7 +60,7 @@ namespace MWSound Misc::ObjectPool mStreams; - typedef std::pair SoundBufferRefPair; + typedef std::pair SoundBufferRefPair; typedef std::vector SoundBufferRefPairList; struct ActiveSound @@ -109,7 +109,7 @@ namespace MWSound Sound* mCurrentRegionSound; - Sound_Buffer* insertSound(const std::string& soundId, const ESM::Sound* sound); + SoundBuffer* insertSound(const std::string& soundId, const ESM::Sound* sound); // returns a decoder to start streaming, or nullptr if the sound was not found DecoderPtr loadVoice(VFS::Path::NormalizedView voicefile); @@ -126,9 +126,9 @@ namespace MWSound bool remove3DSoundAtDistance(PlayMode mode, const MWWorld::ConstPtr& ptr) const; - Sound* playSound(Sound_Buffer* sfx, float volume, float pitch, Type type = Type::Sfx, + Sound* playSound(SoundBuffer* sfx, float volume, float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0); - Sound* playSound3D(const MWWorld::ConstPtr& ptr, Sound_Buffer* sfx, float volume, float pitch, Type type, + Sound* playSound3D(const MWWorld::ConstPtr& ptr, SoundBuffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset); void updateSounds(float duration); @@ -144,7 +144,7 @@ namespace MWSound PlaySound, }; - std::pair getWaterSoundAction( + std::pair getWaterSoundAction( const WaterSoundUpdate& update, const MWWorld::Cell* cell) const; SoundManager(const SoundManager& rhs); @@ -152,9 +152,9 @@ namespace MWSound protected: DecoderPtr getDecoder(); - friend class OpenAL_Output; + friend class OpenALOutput; - void stopSound(Sound_Buffer* sfx, const MWWorld::ConstPtr& ptr); + void stopSound(SoundBuffer* sfx, const MWWorld::ConstPtr& ptr); ///< Stop the given object from playing given sound buffer. public: diff --git a/apps/openmw/mwsound/sound_output.hpp b/apps/openmw/mwsound/soundoutput.hpp similarity index 88% rename from apps/openmw/mwsound/sound_output.hpp rename to apps/openmw/mwsound/soundoutput.hpp index 5a77124985..e5c266b204 100644 --- a/apps/openmw/mwsound/sound_output.hpp +++ b/apps/openmw/mwsound/soundoutput.hpp @@ -1,5 +1,5 @@ -#ifndef GAME_SOUND_SOUND_OUTPUT_H -#define GAME_SOUND_SOUND_OUTPUT_H +#ifndef GAME_SOUND_SOUNDOUTPUT_H +#define GAME_SOUND_SOUNDOUTPUT_H #include #include @@ -13,7 +13,7 @@ namespace MWSound { class SoundManager; - struct Sound_Decoder; + struct SoundDecoder; class Sound; class Stream; @@ -30,7 +30,7 @@ namespace MWSound using HrtfMode = Settings::HrtfMode; - class Sound_Output + class SoundOutput { SoundManager& mManager; @@ -71,24 +71,24 @@ namespace MWSound virtual void pauseActiveDevice() = 0; virtual void resumeActiveDevice() = 0; - Sound_Output& operator=(const Sound_Output& rhs); - Sound_Output(const Sound_Output& rhs); + SoundOutput& operator=(const SoundOutput& rhs); + SoundOutput(const SoundOutput& rhs); protected: bool mInitialized; - Sound_Output(SoundManager& mgr) + SoundOutput(SoundManager& mgr) : mManager(mgr) , mInitialized(false) { } public: - virtual ~Sound_Output() {} + virtual ~SoundOutput() {} bool isInitialized() const { return mInitialized; } - friend class OpenAL_Output; + friend class OpenALOutput; friend class SoundManager; friend class SoundBufferPool; }; diff --git a/apps/openmw/mwstate/character.cpp b/apps/openmw/mwstate/character.cpp index 3c02311458..22d7e7ba1e 100644 --- a/apps/openmw/mwstate/character.cpp +++ b/apps/openmw/mwstate/character.cpp @@ -18,14 +18,25 @@ bool MWState::operator<(const Slot& left, const Slot& right) return left.mTimeStamp < right.mTimeStamp; } -std::string MWState::getFirstGameFile(const std::vector& contentFiles) +bool MWState::operator<(const Character& left, const Character& right) +{ + if (left.mSlots.empty() && right.mSlots.empty()) + return left.mPath < right.mPath; + else if (left.mSlots.empty()) + return false; + else if (right.mSlots.empty()) + return true; + return right.mSlots.back() < left.mSlots.back(); +} + +std::string_view MWState::getFirstGameFile(const std::vector& contentFiles) { for (const std::string& c : contentFiles) { if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) return c; } - return ""; + return {}; } void MWState::Character::addSlot(const std::filesystem::path& path, const std::string& game) diff --git a/apps/openmw/mwstate/character.hpp b/apps/openmw/mwstate/character.hpp index 3c68d9f490..4e51301bba 100644 --- a/apps/openmw/mwstate/character.hpp +++ b/apps/openmw/mwstate/character.hpp @@ -2,6 +2,7 @@ #define GAME_STATE_CHARACTER_H #include +#include #include @@ -14,9 +15,7 @@ namespace MWState std::filesystem::file_time_type mTimeStamp; }; - bool operator<(const Slot& left, const Slot& right); - - std::string getFirstGameFile(const std::vector& contentFiles); + std::string_view getFirstGameFile(const std::vector& contentFiles); class Character { @@ -63,7 +62,12 @@ namespace MWState ///< Return signature information for this character. /// /// \attention This function must not be called if there are no slots. + + friend bool operator<(const Character& left, const Character& right); }; + + bool operator<(const Slot& left, const Slot& right); + bool operator<(const Character& left, const Character& right); } #endif diff --git a/apps/openmw/mwstate/charactermanager.cpp b/apps/openmw/mwstate/charactermanager.cpp index 32f0fe0aef..02d993d186 100644 --- a/apps/openmw/mwstate/charactermanager.cpp +++ b/apps/openmw/mwstate/charactermanager.cpp @@ -18,10 +18,8 @@ MWState::CharacterManager::CharacterManager(std::filesystem::path saves, const s } else { - for (std::filesystem::directory_iterator iter(mPath); iter != std::filesystem::directory_iterator(); ++iter) + for (const std::filesystem::path& characterDir : std::filesystem::directory_iterator(mPath)) { - std::filesystem::path characterDir = *iter; - if (std::filesystem::is_directory(characterDir)) { Character character(characterDir, mGame); @@ -30,6 +28,7 @@ MWState::CharacterManager::CharacterManager(std::filesystem::path saves, const s mCharacters.push_back(character); } } + mCharacters.sort(); } } @@ -82,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 b997d124c8..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; @@ -660,11 +661,13 @@ void MWState::StateManager::loadGame(const Character* character, const std::file // Report the last version still capable of reading this save if (e.getFormatVersion() <= ESM::OpenMW0_48SaveGameFormatVersion) release = "OpenMW 0.48.0"; + else if (e.getFormatVersion() <= ESM::OpenMW0_49SaveGameFormatVersion) + release = "OpenMW 0.49.0"; else { // Insert additional else if statements above to cover future releases - static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_49SaveGameFormatVersion); - release = "OpenMW 0.49.0"; + static_assert(ESM::MinSupportedSaveGameFormatVersion <= ESM::OpenMW0_50SaveGameFormatVersion); + release = "OpenMW 0.50.0"; } auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); std::string error = l10n->formatMessage("LoadingRequiresOldVersionError", { "version" }, { release }); 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/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..86af303341 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.getEffectiveArmorRating(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.getEffectiveArmorRating(test, actor); + const float oldRating = oldCls.getEffectiveArmorRating(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/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index b019dabdfe..6ea510e1e2 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -274,8 +274,8 @@ namespace MWWorld pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight; } - if (MWBase::Environment::get().getWorld()->isUnderwater( - caster.getCell(), pos)) // Underwater casting not possible + // Actors can't cast target spells underwater + if (caster.getClass().isActor() && MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) return; osg::Quat orient; @@ -564,15 +564,19 @@ namespace MWWorld for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); - if (projectile->isActive()) + const Ptr caster = magicBoltState.getCaster(); + + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const bool active = projectile->isActive(); + if (active && !world.isUnderwater(caster.getCell(), pos)) continue; - const auto target = projectile->getTarget(); - const auto caster = magicBoltState.getCaster(); + const Ptr target = !active ? projectile->getTarget() : Ptr(); + assert(target != caster); MWMechanics::CastSpell cast(caster, target); - cast.mHitPosition = Misc::Convert::toOsg(projectile->getHitPosition()); + cast.mHitPosition = !active ? Misc::Convert::toOsg(projectile->getHitPosition()) : pos; cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; cast.mItem = magicBoltState.mItem; diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index fb3aee958c..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" @@ -375,7 +376,6 @@ namespace MWWorld if (object->getShapeInstance()->mVisualCollisionType == Resource::VisualCollisionType::None) mNavigator.removeObject(DetourNavigator::ObjectId(object), navigatorUpdateGuard); mPhysics->remove(ptr); - ptr.mRef->mData.mPhysicsPostponed = false; } else if (mPhysics->getActor(ptr)) { @@ -383,6 +383,8 @@ namespace MWWorld mRendering.removeActorPath(ptr); mPhysics->remove(ptr); } + else + ptr.mRef->mData.mPhysicsPostponed = false; MWBase::Environment::get().getLuaManager()->objectRemovedFromScene(ptr); } @@ -510,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); } @@ -644,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); @@ -1291,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 4f6f52a81a..2ee77458d4 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -352,6 +352,30 @@ namespace MWWorld mWeather = 0; } + MoonModel::MoonModel(float fadeInStart, float fadeInFinish, float fadeOutStart, float fadeOutFinish, + float axisOffset, float speed, float dailyIncrement, float fadeStartAngle, float fadeEndAngle, + float moonShadowEarlyFadeAngle) + { + mFadeInStart = fadeInStart; + mFadeInFinish = fadeInFinish; + mFadeOutStart = fadeOutStart; + mFadeOutFinish = fadeOutFinish; + mAxisOffset = axisOffset; + mDailyIncrement = dailyIncrement; + mFadeStartAngle = fadeStartAngle; + mFadeEndAngle = fadeEndAngle; + mMoonShadowEarlyFadeAngle = moonShadowEarlyFadeAngle; + + // Morrowind appears to have a minimum speed to avoid situations where the moon can't + // complete a full rotation in a single 24-hour period. The reverse-engineered formula is + // 180 degrees (full hemisphere) / 23 hours / 15 degrees (1 hour travel at speed 1.0). + mSpeed = std::max(speed, (180.0f / 23.0f / 15.0f)); + + // Morrowind appears to reduce mDailyIncrement with modulo 24.0f to avoid situations where + // the moon would increment more than an entire rotation in a single day. + mDailyIncrement = std::fmod(mDailyIncrement, 24.0f); + } + MoonModel::MoonModel(const std::string& name) : mFadeInStart(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Start")) , mFadeInFinish(Fallback::Map::getFloat("Moons_" + name + "_Fade_In_Finish")) @@ -364,14 +388,19 @@ namespace MWWorld , mFadeEndAngle(Fallback::Map::getFloat("Moons_" + name + "_Fade_End_Angle")) , mMoonShadowEarlyFadeAngle(Fallback::Map::getFloat("Moons_" + name + "_Moon_Shadow_Early_Fade_Angle")) { - // Morrowind appears to have a minimum speed in order to avoid situations where the moon couldn't conceivably - // complete a rotation in a single 24 hour period. The value of 180/23 was deduced from reverse engineering. - mSpeed = std::min(mSpeed, 180.0f / 23.0f); + // Morrowind appears to have a minimum speed to avoid situations where the moon can't + // complete a full rotation in a single 24-hour period. The reverse-engineered formula is + // 180 degrees (full hemisphere) / 23 hours / 15 degrees (1 hour travel at speed 1.0). + mSpeed = std::max(mSpeed, (180.0f / 23.0f / 15.0f)); + + // Morrowind appears to reduce mDailyIncrement with modulo 24.0f to avoid situations where + // the moon would increment more than an entire rotation in a single day. + mDailyIncrement = std::fmod(mDailyIncrement, 24.0f); } MWRender::MoonState MoonModel::calculateState(const TimeStamp& gameTime) const { - float rotationFromHorizon = angle(gameTime); + float rotationFromHorizon = angle(gameTime.getDay(), gameTime.getHour()); MWRender::MoonState state = { rotationFromHorizon, mAxisOffset, // Reverse engineered from Morrowind's scene graph rotation matrices. phase(gameTime), shadowBlend(rotationFromHorizon), @@ -380,7 +409,7 @@ namespace MWWorld return state; } - inline float MoonModel::angle(const TimeStamp& gameTime) const + inline float MoonModel::angle(int gameDay, float gameHour) const { // Morrowind's moons start travel on one side of the horizon (let's call it H-rise) and travel 180 degrees to // the opposite horizon (let's call it H-set). Upon reaching H-set, they reset to H-rise until the next moon @@ -390,49 +419,83 @@ namespace MWWorld // 1. Moon rises and then sets in one day. // 2. Moon sets and doesn't rise in one day (occurs when the moon rise hour is >= 24). // 3. Moon sets and then rises in one day. - float moonRiseHourToday = moonRiseHour(gameTime.getDay()); - float moonRiseAngleToday = 0; + float moonRiseHourToday = moonRiseHour(gameDay); + float moonRiseAngleToday = 0.0f; - if (gameTime.getHour() < moonRiseHourToday) + if (gameHour < moonRiseHourToday) { - float moonRiseHourYesterday = moonRiseHour(gameTime.getDay() - 1); - if (moonRiseHourYesterday < 24) + // Rise hour increases by mDailyIncrement each day, so yesterday's is easy to calculate + float moonRiseHourYesterday = moonRiseHourToday - mDailyIncrement; + if (moonRiseHourYesterday < 24.0f) { - float moonRiseAngleYesterday = rotation(24 - moonRiseHourYesterday); - if (moonRiseAngleYesterday < 180) + // Morrowind offsets the increment by -1 when the previous day's visible point crosses into the next + // day. The offset lasts from this point until the next 24-day loop starts. To find this point we add + // mDailyIncrement to the previous visible point and check the result. + float moonShadowEarlyFadeAngle1 = mFadeEndAngle - mMoonShadowEarlyFadeAngle; + float timeToVisible = moonShadowEarlyFadeAngle1 / rotation(1.0f); + float cycleOffset = moonRiseHourYesterday + timeToVisible > 24.0f ? mDailyIncrement : 0.0f; + + float moonRiseAngleYesterday = rotation(24.0f - (moonRiseHourYesterday + cycleOffset)); + if (moonRiseAngleYesterday < 180.0f) { // The moon rose but did not set yesterday, so accumulate yesterday's angle with how much we've // travelled today. - moonRiseAngleToday = rotation(gameTime.getHour()) + moonRiseAngleYesterday; + moonRiseAngleToday = rotation(gameHour) + moonRiseAngleYesterday; } } } else { - moonRiseAngleToday = rotation(gameTime.getHour() - moonRiseHourToday); + moonRiseAngleToday = rotation(gameHour - moonRiseHourToday); } - if (moonRiseAngleToday >= 180) + if (moonRiseAngleToday >= 180.0f) { // The moon set today, reset the angle to the horizon. - moonRiseAngleToday = 0; + moonRiseAngleToday = 0.0f; } return moonRiseAngleToday; } - inline float MoonModel::moonRiseHour(unsigned int daysPassed) const + inline float MoonModel::moonPhaseHour(int gameDay) const { + // Morrowind delays moon phase changes until one of these is true: + // * The moon is invisible at midnight. + // * The moon reached moonShadowEarlyFadeAngle2 one daily increment ago (therefore invisible). + if (!isVisible(gameDay, 0.0f)) + return 0.0f; + else + { + // Calculate the angle at which the moon becomes transparent and the starting angle. + float moonShadowEarlyFadeAngle2 = (180.0f - mFadeEndAngle) + mMoonShadowEarlyFadeAngle; + float midnightAngle = angle(gameDay, 0.0f); + + // We can assume that moonShadowEarlyFadeAngle2 > midnightAngle, because the opposite + // case would make the moon invisible at midnight, which is checked above. + return ((moonShadowEarlyFadeAngle2 - midnightAngle) / rotation(1.0f)) + std::max(mDailyIncrement, 0.0f); + } + } + + inline float MoonModel::moonRiseHour(int gameDay) const + { + if (mDailyIncrement == 0.0f) + return 0.0f; + // This arises from the start date of 16 Last Seed, 427 // TODO: Find an alternate formula that doesn't rely on this day being fixed. - static const unsigned int startDay = 16; + constexpr int startDay = 16; + + // This formula finds the number of missed increments necessary to make the rise hour a 24-day loop. + // The offset increases on the first day of the loop and is multiplied by the number of completed loops. + float incrementOffset = (24.0f - std::abs(24.0f / mDailyIncrement)) * std::floor((gameDay + startDay) / 24.0f); // This odd formula arises from the fact that on 16 Last Seed, 17 increments have occurred, meaning // that upon starting a new game, it must only calculate the moon phase as far back as 1 Last Seed. // Note that we don't modulo after adding the latest daily increment because other calculations need to // know if doing so would cause the moon rise to be postponed until the next day (which happens when // the moon rise hour is >= 24 in Morrowind). - return mDailyIncrement + std::fmod((daysPassed - 1 + startDay) * mDailyIncrement, 24.0f); + return mDailyIncrement + std::fmod((gameDay - 1 + startDay - incrementOffset) * mDailyIncrement, 24.0f); } inline float MoonModel::rotation(float hours) const @@ -449,12 +512,18 @@ namespace MWWorld // phase cycle. // If the moon didn't rise yet today, use yesterday's moon phase. - if (gameTime.getHour() < moonRiseHour(gameTime.getDay())) + if (gameTime.getHour() < moonPhaseHour(gameTime.getDay())) return static_cast((gameTime.getDay() / 3) % 8); else return static_cast(((gameTime.getDay() + 1) / 3) % 8); } + inline bool MoonModel::isVisible(int gameDay, float gameHour) const + { + // Moons are "visible" when their alpha value is non-zero. + return hourlyAlpha(gameHour) > 0.f && earlyMoonShadowAlpha(angle(gameDay, gameHour)) > 0.f; + } + inline float MoonModel::shadowBlend(float angle) const { // The Fade End Angle and Fade Start Angle describe a region where the moon transitions from a solid disk @@ -480,6 +549,10 @@ namespace MWWorld inline float MoonModel::hourlyAlpha(float gameHour) const { + // Morrowind culls the moon one minute before mFadeOutFinish + constexpr float oneMinute = 0.0167f; + float adjustedFadeOutFinish = mFadeOutFinish - oneMinute; + // The Fade Out Start / Finish and Fade In Start / Finish describe the hours at which the moon // appears and disappears. // Depending on the current hour, the following values describe how transparent the moon is. @@ -487,9 +560,9 @@ namespace MWWorld // 2. From Fade Out Finish to Fade In Start: 0 (transparent) // 3. From Fade In Start to Fade In Finish: 0..1 // 4. From Fade In Finish to Fade Out Start: 1 (solid) - if ((gameHour >= mFadeOutStart) && (gameHour < mFadeOutFinish)) - return (mFadeOutFinish - gameHour) / (mFadeOutFinish - mFadeOutStart); - else if ((gameHour >= mFadeOutFinish) && (gameHour < mFadeInStart)) + if ((gameHour >= mFadeOutStart) && (gameHour < adjustedFadeOutFinish)) + return (adjustedFadeOutFinish - gameHour) / (adjustedFadeOutFinish - mFadeOutStart); + else if ((gameHour >= adjustedFadeOutFinish) && (gameHour < mFadeInStart)) return 0.0f; else if ((gameHour >= mFadeInStart) && (gameHour < mFadeInFinish)) return (gameHour - mFadeInStart) / (mFadeInFinish - mFadeInStart); diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 0643240dcd..7c27a10316 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -251,6 +251,9 @@ namespace MWWorld { public: MoonModel(const std::string& name); + MoonModel(float fadeInStart, float fadeInFinish, float fadeOutStart, float fadeOutFinish, float axisOffset, + float speed, float dailyIncrement, float fadeStartAngle, float fadeEndAngle, + float moonShadowEarlyFadeAngle); MWRender::MoonState calculateState(const TimeStamp& gameTime) const; @@ -266,10 +269,12 @@ namespace MWWorld float mFadeEndAngle; float mMoonShadowEarlyFadeAngle; - float angle(const TimeStamp& gameTime) const; - float moonRiseHour(unsigned int daysPassed) const; + float angle(int gameDay, float gameHour) const; + float moonPhaseHour(int gameDay) const; + float moonRiseHour(int gameDay) const; float rotation(float hours) const; MWRender::MoonState::Phase phase(const TimeStamp& gameTime) const; + bool isVisible(int gameDay, float gameHour) const; float shadowBlend(float angle) const; float hourlyAlpha(float gameHour) const; float earlyMoonShadowAlpha(float angle) const; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 57d794c535..97788669d5 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 @@ -2333,34 +2326,35 @@ namespace MWWorld Log(Debug::Warning) << "Player agent bounds are not supported by navigator: " << agentBounds; } - World::RestPermitted World::canRest() const + int World::canRest() const { + int result = 0; + CellStore* currentCell = mWorldScene->getCurrentCell(); Ptr player = mPlayer->getPlayer(); - RefData& refdata = player.getRefData(); - osg::Vec3f playerPos(refdata.getPosition().asVec3()); const MWPhysics::Actor* actor = mPhysics->getActor(player); if (!actor) throw std::runtime_error("can't find player"); - if (mPlayer->enemiesNearby()) - return Rest_EnemiesAreNearby; - + const osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); if (isUnderwater(currentCell, playerPos) || isWalkingOnWater(player)) - return Rest_PlayerIsUnderwater; + result |= Rest_PlayerIsUnderwater; float fallHeight = player.getClass().getCreatureStats(player).getFallHeight(); float epsilon = 1e-4; if ((actor->getCollisionMode() && (!mPhysics->isOnSolidGround(player) || fallHeight >= epsilon)) || isFlying(player)) - return Rest_PlayerIsInAir; + result |= Rest_PlayerIsInAir; - if (currentCell->getCell()->noSleep() || player.getClass().getNpcStats(player).isWerewolf()) - return Rest_OnlyWaiting; + if (mPlayer->enemiesNearby()) + result |= Rest_EnemiesAreNearby; - return Rest_Allowed; + if (!currentCell->getCell()->noSleep() && !player.getClass().getNpcStats(player).isWerewolf()) + result |= Rest_CanSleep; + + return result; } MWRender::Animation* World::getAnimation(const MWWorld::Ptr& ptr) @@ -3047,28 +3041,31 @@ namespace MWWorld // TODO: as a better solutuon we should handle projectiles during physics update, not during world update. const osg::Vec3f sourcePos = worldPos + orient * osg::Vec3f(0, -1, 0) * 64.f; - // Early out if the launch position is underwater - bool underwater = isUnderwater(MWMechanics::getPlayer().getCell(), worldPos); - if (underwater) - { - MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); - mRendering->emitWaterRipple(worldPos); - return; - } - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit // result. std::vector targetActors; if (!actor.isEmpty() && actor.getClass().isActor() && actor != MWMechanics::getPlayer()) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); - // Check for impact, if yes, handle hit, if not, launch projectile + // Check for impact, if yes, handle hit MWPhysics::RayCastingResult result = mPhysics->castRay( sourcePos, worldPos, { actor }, targetActors, 0xff, MWPhysics::CollisionType_Projectile); + if (result.mHit) + { MWMechanics::projectileHit(actor, result.mHitObject, bow, projectile, result.mHitPos, attackStrength); - else - mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); + return; + } + + // Bail out if the launch position is underwater + if (isUnderwater(MWMechanics::getPlayer().getCell(), worldPos)) + { + MWMechanics::projectileHit(actor, Ptr(), bow, projectile, worldPos, attackStrength); + mRendering->emitWaterRipple(worldPos); + return; + } + + mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); } void World::launchMagicBolt( diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 983682a98f..b1286d5532 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -505,7 +505,7 @@ namespace MWWorld void enableActorCollision(const MWWorld::Ptr& actor, bool enable) override; - RestPermitted canRest() const override; + int canRest() const override; ///< check if the player is allowed to rest void rest(double hours) override; diff --git a/apps/openmw_tests/CMakeLists.txt b/apps/openmw_tests/CMakeLists.txt index 9b57113110..adfce32537 100644 --- a/apps/openmw_tests/CMakeLists.txt +++ b/apps/openmw_tests/CMakeLists.txt @@ -6,14 +6,15 @@ file(GLOB UNITTEST_SRC_FILES options.cpp - mwworld/test_store.cpp + mwworld/teststore.cpp mwworld/testduration.cpp mwworld/testtimestamp.cpp mwworld/testptr.cpp + mwworld/testweather.cpp - mwdialogue/test_keywordsearch.cpp + mwdialogue/testkeywordsearch.cpp - mwscript/test_scripts.cpp + mwscript/testscripts.cpp ) source_group(apps\\openmw-tests FILES ${UNITTEST_SRC_FILES}) diff --git a/apps/openmw_tests/mwdialogue/test_keywordsearch.cpp b/apps/openmw_tests/mwdialogue/testkeywordsearch.cpp similarity index 68% rename from apps/openmw_tests/mwdialogue/test_keywordsearch.cpp rename to apps/openmw_tests/mwdialogue/testkeywordsearch.cpp index a3f0d8d3c0..0eb996019e 100644 --- a/apps/openmw_tests/mwdialogue/test_keywordsearch.cpp +++ b/apps/openmw_tests/mwdialogue/testkeywordsearch.cpp @@ -46,7 +46,7 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution2) TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) { - // testing that the longest keyword is chosen, rather than maximizing the + // Test that the longest keyword is chosen, rather than maximizing the // amount of highlighted characters by highlighting the first and last keyword MWDialogue::KeywordSearch search; search.seed("foo bar", 0); @@ -64,12 +64,12 @@ TEST_F(KeywordSearchTest, keyword_test_conflict_resolution3) TEST_F(KeywordSearchTest, keyword_test_utf8_word_begin) { - // We make sure that the search works well even if the character is not ASCII + // Make sure that the search works well on UTF-8 strings containing some non-ASCII (French) MWDialogue::KeywordSearch search; search.seed("états", 0); search.seed("ïrradiés", 0); search.seed("ça nous déçois", 0); - search.seed("ois", 0); + search.seed("nous", 0); std::string text = "les nations unis ont réunis le monde entier, états units inclus pour parler du problème des gens ïrradiés " @@ -86,38 +86,22 @@ TEST_F(KeywordSearchTest, keyword_test_utf8_word_begin) TEST_F(KeywordSearchTest, keyword_test_non_alpha_non_whitespace_word_begin) { - // We make sure that the search works well even if the separator is not a whitespace + // Make sure that the search works well even if the separator is not whitespace MWDialogue::KeywordSearch search; search.seed("Report to caius cosades", 0); - std::string text = "I was told to \"Report to caius cosades\""; + std::string text = "I was told to \"Report to Caius Cosades\""; std::vector::Match> matches; search.highlightKeywords(text.begin(), text.end(), matches); EXPECT_EQ(matches.size(), 1); - EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Report to caius cosades"); -} - -TEST_F(KeywordSearchTest, keyword_test_russian_non_ascii_before) -{ - // We make sure that the search works well even if the separator is not a whitespace with russian chars - MWDialogue::KeywordSearch search; - search.seed("Доложить Каю Косадесу", 0); - - std::string text - = "Что? Да. Я Кай Косадес. То есть как это, вам велели «Доложить Каю Косадесу»? О чем вы говорите?"; - - std::vector::Match> matches; - search.highlightKeywords(text.begin(), text.end(), matches); - - EXPECT_EQ(matches.size(), 1); - EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Report to Caius Cosades"); } TEST_F(KeywordSearchTest, keyword_test_russian_ascii_before) { - // We make sure that the search works well even if the separator is not a whitespace with russian chars + // Make sure that the search works well even if the separator is not whitespace with Russian chars MWDialogue::KeywordSearch search; search.seed("Доложить Каю Косадесу", 0); @@ -130,3 +114,53 @@ TEST_F(KeywordSearchTest, keyword_test_russian_ascii_before) EXPECT_EQ(matches.size(), 1); EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Доложить Каю Косадесу"); } + +TEST_F(KeywordSearchTest, keyword_test_substrings_without_word_separators) +{ + // Make sure that the search does not highlight substrings within words + // i.e. "Force" does not contain "orc" + // and "bring" does not contain "ring" + MWDialogue::KeywordSearch search; + search.seed("orc", 0); + search.seed("ring", 0); + + std::string text = "Bring the Force, Lucan!"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 0); +} + +TEST_F(KeywordSearchTest, keyword_test_initial_substrings_match) +{ + // Make sure that the search highlights prefix substrings + // "Orcs" should match "orc" + // "ring" is not matched because "-" is not a word separator + MWDialogue::KeywordSearch search; + search.seed("orc", 0); + search.seed("ring", 0); + + std::string text = "Bring the Orcs some gold-rings."; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 1); + EXPECT_EQ(std::string(matches[0].mBeg, matches[0].mEnd), "Orc"); +} + +TEST_F(KeywordSearchTest, keyword_test_french_substrings) +{ + // Substrings within words should not match + MWDialogue::KeywordSearch search; + search.seed("ages", 0); + search.seed("orc", 0); + + std::string text = "traçages et forces"; + + std::vector::Match> matches; + search.highlightKeywords(text.begin(), text.end(), matches); + + EXPECT_EQ(matches.size(), 0); +} diff --git a/apps/openmw_tests/mwscript/test_scripts.cpp b/apps/openmw_tests/mwscript/testscripts.cpp similarity index 99% rename from apps/openmw_tests/mwscript/test_scripts.cpp rename to apps/openmw_tests/mwscript/testscripts.cpp index 0de542bdc6..b9e422daed 100644 --- a/apps/openmw_tests/mwscript/test_scripts.cpp +++ b/apps/openmw_tests/mwscript/testscripts.cpp @@ -1,7 +1,7 @@ #include #include -#include "test_utils.hpp" +#include "testutils.hpp" namespace { @@ -935,4 +935,4 @@ End)mwscript"; registerExtensions(); EXPECT_FALSE(!compile(sIssue6807)); } -} \ No newline at end of file +} diff --git a/apps/openmw_tests/mwscript/test_utils.hpp b/apps/openmw_tests/mwscript/testutils.hpp similarity index 100% rename from apps/openmw_tests/mwscript/test_utils.hpp rename to apps/openmw_tests/mwscript/testutils.hpp diff --git a/apps/openmw_tests/mwworld/test_store.cpp b/apps/openmw_tests/mwworld/teststore.cpp similarity index 100% rename from apps/openmw_tests/mwworld/test_store.cpp rename to apps/openmw_tests/mwworld/teststore.cpp diff --git a/apps/openmw_tests/mwworld/testweather.cpp b/apps/openmw_tests/mwworld/testweather.cpp new file mode 100644 index 0000000000..4b7fd8f9e2 --- /dev/null +++ b/apps/openmw_tests/mwworld/testweather.cpp @@ -0,0 +1,732 @@ +#include + +#include + +#include "apps/openmw/mwworld/timestamp.hpp" +#include "apps/openmw/mwworld/weather.hpp" + +namespace MWWorld +{ + namespace + { + // MASSER PHASES + + TEST(MWWorldWeatherTest, masserPhasesFullToWaningGibbousAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 2 and 26, 11:57 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 2 + 11.0f + 56.0f / 60.0f); + timeStampAfter += (24.0f * 2 + 11.0f + 58.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 26 + 11.0f + 56.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 26 + 11.0f + 58.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(0)); + EXPECT_EQ(afterState.mPhase, static_cast(1)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(0)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(1)); + } + + TEST(MWWorldWeatherTest, masserPhasesWaningGibbousToThirdQuarterAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 5 and 29, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 4 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 5 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 28 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 29 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(1)); + EXPECT_EQ(afterState.mPhase, static_cast(2)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(1)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(2)); + } + + TEST(MWWorldWeatherTest, masserPhasesThirdQuarterToWaningCrescentAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 8 and 32, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 7 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 8 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 31 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 32 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(2)); + EXPECT_EQ(afterState.mPhase, static_cast(3)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(2)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(3)); + } + + TEST(MWWorldWeatherTest, masserPhasesWaningCrescentToNewAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 11 and 35, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 10 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 11 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 34 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 35 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(3)); + EXPECT_EQ(afterState.mPhase, static_cast(4)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(3)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(4)); + } + + TEST(MWWorldWeatherTest, masserPhasesNewToWaxingCrescentAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 14 and 38, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 13 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 14 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 37 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 38 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(4)); + EXPECT_EQ(afterState.mPhase, static_cast(5)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(4)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(5)); + } + + TEST(MWWorldWeatherTest, masserPhasesWaxingCrescentToFirstQuarterAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 17 and 41, 2:57 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 17 + 2.0f + 56.0f / 60.0f); + timeStampAfter += (24.0f * 17 + 2.0f + 58.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 41 + 2.0f + 56.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 41 + 2.0f + 58.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(5)); + EXPECT_EQ(afterState.mPhase, static_cast(6)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(5)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(6)); + } + + TEST(MWWorldWeatherTest, masserPhasesFirstQuarterToWaxingGibbousAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 20 and 44, 5:57 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 20 + 5.0f + 56.0f / 60.0f); + timeStampAfter += (24.0f * 20 + 5.0f + 58.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 44 + 5.0f + 56.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 44 + 5.0f + 58.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(6)); + EXPECT_EQ(afterState.mPhase, static_cast(7)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(6)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(7)); + } + + TEST(MWWorldWeatherTest, masserPhasesWaxingGibbousToFullAtCorrectTimes) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 35.0f; + + // Days 23 and 47, 8:57 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 23 + 8.0f + 56.0f / 60.0f); + timeStampAfter += (24.0f * 23 + 8.0f + 58.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 47 + 8.0f + 56.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 47 + 8.0f + 58.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(7)); + EXPECT_EQ(afterState.mPhase, static_cast(0)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(7)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(0)); + } + + // SECUNDA PHASES + + TEST(MWWorldWeatherTest, secundaPhasesFullToWaningGibbousAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 2 and 26, 14:19 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 2 + 14.0f + 18.0f / 60.0f); + timeStampAfter += (24.0f * 2 + 14.0f + 20.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 26 + 14.0f + 18.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 26 + 14.0f + 20.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(0)); + EXPECT_EQ(afterState.mPhase, static_cast(1)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(0)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(1)); + } + + TEST(MWWorldWeatherTest, secundaPhasesWaningGibbousToThirdQuarterAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 5 and 29, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 4 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 5 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 28 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 29 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(1)); + EXPECT_EQ(afterState.mPhase, static_cast(2)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(1)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(2)); + } + + TEST(MWWorldWeatherTest, secundaPhasesThirdQuarterToWaningCrescentAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 8 and 32, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 7 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 8 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 31 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 32 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(2)); + EXPECT_EQ(afterState.mPhase, static_cast(3)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(2)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(3)); + } + + TEST(MWWorldWeatherTest, secundaPhasesWaningCrescentToNewAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 11 and 35, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 10 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 11 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 34 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 35 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(3)); + EXPECT_EQ(afterState.mPhase, static_cast(4)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(3)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(4)); + } + + TEST(MWWorldWeatherTest, secundaPhasesNewToWaxingCrescentAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 14 and 38, 0:00 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 13 + 23.0f + 59.0f / 60.0f); + timeStampAfter += (24.0f * 14 + 0.0f + 1.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 37 + 23.0f + 59.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 38 + 0.0f + 1.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(4)); + EXPECT_EQ(afterState.mPhase, static_cast(5)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(4)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(5)); + } + + TEST(MWWorldWeatherTest, secundaPhasesWaxingCrescentToFirstQuarterAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 17 and 41, 3:31 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 17 + 3.0f + 30.0f / 60.0f); + timeStampAfter += (24.0f * 17 + 3.0f + 32.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 41 + 3.0f + 30.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 41 + 3.0f + 32.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(5)); + EXPECT_EQ(afterState.mPhase, static_cast(6)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(5)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(6)); + } + + TEST(MWWorldWeatherTest, secundaPhasesFirstQuarterToWaxingGibbousAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 20 and 44, 7:07 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 20 + 7.0f + 6.0f / 60.0f); + timeStampAfter += (24.0f * 20 + 7.0f + 8.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 44 + 7.0f + 6.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 44 + 7.0f + 8.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(6)); + EXPECT_EQ(afterState.mPhase, static_cast(7)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(6)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(7)); + } + + TEST(MWWorldWeatherTest, secundaPhasesWaxingGibbousToFullAtCorrectTimes) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 23 and 47, 10:43 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 23 + 10.0f + 42.0f / 60.0f); + timeStampAfter += (24.0f * 23 + 10.0f + 44.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 47 + 10.0f + 42.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 47 + 10.0f + 44.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_EQ(beforeState.mPhase, static_cast(7)); + EXPECT_EQ(afterState.mPhase, static_cast(0)); + EXPECT_EQ(beforeStatePostLoop.mPhase, static_cast(7)); + EXPECT_EQ(afterStatePostLoop.mPhase, static_cast(0)); + } + + // OFFSETS + + TEST(MWWorldWeatherTest, secundaShouldApplyIncrementOffsetAfterFirstLoop) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 14.0f; + float fadeInFinish = 15.0f; + float fadeOutStart = 7.0f; + float fadeOutFinish = 10.0f; + float axisOffset = 50.0f; + + // Days 8 and 32, 3:16 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 8 + 3.0f + 15.0f / 60.0f); + timeStampAfter += (24.0f * 8 + 3.0f + 17.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 32 + 3.0f + 15.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 32 + 3.0f + 17.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_LE(beforeState.mMoonAlpha, 0.0f); + EXPECT_GT(afterState.mMoonAlpha, 0.0f); + EXPECT_LE(beforeStatePostLoop.mMoonAlpha, 0.0f); + EXPECT_GT(afterStatePostLoop.mMoonAlpha, 0.0f); + } + + TEST(MWWorldWeatherTest, moonWithLowIncrementShouldApplyIncrementOffsetAfterCycle) + { + float dailyIncrement = 0.9f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 0.0f; + float fadeInFinish = 0.0f; + float fadeOutStart = 0.0f; + float fadeOutFinish = 0.0f; + float axisOffset = 35.0f; + + // Days 7 and 31, 1:44 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 7 + 1.0f + 43.0f / 60.0f); + timeStampAfter += (24.0f * 7 + 1.0f + 45.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 31 + 1.0f + 43.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 31 + 1.0f + 45.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_LE(beforeState.mMoonAlpha, 0.0f); + EXPECT_GT(afterState.mMoonAlpha, 0.0f); + EXPECT_LE(beforeStatePostLoop.mMoonAlpha, 0.0f); + EXPECT_GT(afterStatePostLoop.mMoonAlpha, 0.0f); + } + + TEST(MWWorldWeatherTest, masserShouldApplyIncrementOffsetAfterCycle) + { + float dailyIncrement = 1.0f; + float speed = 0.5f; + float fadeEndAngle = 40.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 0.0f; + float fadeInFinish = 0.0f; + float fadeOutStart = 0.0f; + float fadeOutFinish = 0.0f; + float axisOffset = 35.0f; + + // Days 4 and 28, 1:02 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 4 + 1.0f + 1.0f / 60.0f); + timeStampAfter += (24.0f * 4 + 1.0f + 3.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 28 + 1.0f + 1.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 28 + 1.0f + 3.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_LE(beforeState.mMoonAlpha, 0.0f); + EXPECT_GT(afterState.mMoonAlpha, 0.0f); + EXPECT_LE(beforeStatePostLoop.mMoonAlpha, 0.0f); + EXPECT_GT(afterStatePostLoop.mMoonAlpha, 0.0f); + } + + TEST(MWWorldWeatherTest, secundaShouldApplyIncrementOffsetAfterCycle) + { + float dailyIncrement = 1.2f; + float speed = 0.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 0.0f; + float fadeInFinish = 0.0f; + float fadeOutStart = 0.0f; + float fadeOutFinish = 0.0f; + float axisOffset = 50.0f; + + // Days 3 and 27, 2:04 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 3 + 2.0f + 3.0f / 60.0f); + timeStampAfter += (24.0f * 3 + 2.0f + 5.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 27 + 2.0f + 3.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 27 + 2.0f + 5.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_LE(beforeState.mMoonAlpha, 0.0f); + EXPECT_GT(afterState.mMoonAlpha, 0.0f); + EXPECT_LE(beforeStatePostLoop.mMoonAlpha, 0.0f); + EXPECT_GT(afterStatePostLoop.mMoonAlpha, 0.0f); + } + + TEST(MWWorldWeatherTest, moonWithIncreasedSpeedShouldApplyIncrementOffsetAfterCycle) + { + float dailyIncrement = 1.2f; + float speed = 1.6f; + float fadeEndAngle = 30.0f; + float fadeStartAngle = 50.0f; + float moonShadowEarlyFadeAngle = 0.5f; + float fadeInStart = 0.0f; + float fadeInFinish = 0.0f; + float fadeOutStart = 0.0f; + float fadeOutFinish = 0.0f; + float axisOffset = 50.0f; + + // Days 4 and 28, 1:13 + TimeStamp timeStampBefore, timeStampAfter, timeStampBeforePostLoop, timeStampAfterPostLoop; + timeStampBefore += (24.0f * 4 + 1.0f + 12.0f / 60.0f); + timeStampAfter += (24.0f * 4 + 1.0f + 14.0f / 60.0f); + timeStampBeforePostLoop += (24.0f * 28 + 1.0f + 12.0f / 60.0f); + timeStampAfterPostLoop += (24.0f * 28 + 1.0f + 14.0f / 60.0f); + + MWWorld::MoonModel moon = MWWorld::MoonModel(fadeInStart, fadeInFinish, fadeOutStart, fadeOutFinish, + axisOffset, speed, dailyIncrement, fadeStartAngle, fadeEndAngle, moonShadowEarlyFadeAngle); + + MWRender::MoonState beforeState = moon.calculateState(timeStampBefore); + MWRender::MoonState afterState = moon.calculateState(timeStampAfter); + MWRender::MoonState beforeStatePostLoop = moon.calculateState(timeStampBeforePostLoop); + MWRender::MoonState afterStatePostLoop = moon.calculateState(timeStampAfterPostLoop); + + EXPECT_LE(beforeState.mMoonAlpha, 0.0f); + EXPECT_GT(afterState.mMoonAlpha, 0.0f); + EXPECT_LE(beforeStatePostLoop.mMoonAlpha, 0.0f); + EXPECT_GT(afterStatePostLoop.mMoonAlpha, 0.0f); + } + } +} diff --git a/apps/wizard/inisettings.hpp b/apps/wizard/inisettings.hpp index c8cd30c3c1..6ea8fc4adb 100644 --- a/apps/wizard/inisettings.hpp +++ b/apps/wizard/inisettings.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include namespace Wizard { 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 7659de0ffd..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 @@ -111,7 +112,7 @@ add_component_dir (bgsm ) add_component_dir (bsa - bsa_file compressedbsafile ba2gnrlfile ba2dx10file ba2file memorystream + bsafile compressedbsafile ba2gnrlfile ba2dx10file ba2file memorystream ) add_component_dir (bullethelpers @@ -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 @@ -155,9 +156,9 @@ add_component_dir (nifbullet bulletnifloader ) -add_component_dir (to_utf8 - tables_gen - to_utf8 +add_component_dir (toutf8 + tablesgen + toutf8 ) add_component_dir(esm attr common defs esmcommon records util luascripts format refid esmbridge esmterrain @@ -173,7 +174,7 @@ add_component_dir(esm attr common defs esmcommon records util luascripts format exteriorcelllocation ) -add_component_dir(fx pass technique lexer lexer_types parse_constants widgets stateupdater) +add_component_dir(fx pass technique lexer lexertypes parseconstants widgets stateupdater) add_component_dir(std140 ubo) @@ -362,7 +363,7 @@ add_component_dir (fontloader add_component_dir (sdlutil events - gl4es_init + gl4esinit imagetosurface sdlcursormanager sdlgraphicswindow @@ -390,10 +391,10 @@ copy_resource_file("lua_ui/content.lua" "${OPENMW_RESOURCES_ROOT}" "resources/lu if(WIN32) add_component_dir (crashcatcher - windows_crashcatcher - windows_crashmonitor - windows_crashshm + windowscrashcatcher windowscrashdumppathhelpers + windowscrashmonitor + windowscrashshm ) elseif(NOT ANDROID) add_component_dir (crashcatcher @@ -500,15 +501,15 @@ add_component_dir(platform if (WIN32) add_component_dir(platform - file.win32 + filewin32 ) elseif (UNIX) add_component_dir(platform - file.posix + fileposix ) else () add_component_dir(platform - file.stdio + filestdio ) endif() diff --git a/components/bsa/ba2dx10file.cpp b/components/bsa/ba2dx10file.cpp index 502ca043ab..afb2c0a4aa 100644 --- a/components/bsa/ba2dx10file.cpp +++ b/components/bsa/ba2dx10file.cpp @@ -1,36 +1,21 @@ #include "ba2dx10file.hpp" +#include #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 #include #include #include +#include "ba2file.hpp" +#include "memorystream.hpp" + namespace Bsa { BA2DX10File::BA2DX10File() {} @@ -642,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); @@ -656,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 59db10745b..c902a4ccb0 100644 --- a/components/bsa/ba2dx10file.hpp +++ b/components/bsa/ba2dx10file.hpp @@ -1,5 +1,5 @@ -#ifndef BSA_BA2_DX10_FILE_H -#define BSA_BA2_DX10_FILE_H +#ifndef OPENMW_COMPONENTS_BSA_BA2DX10FILE_HPP +#define OPENMW_COMPONENTS_BSA_BA2DX10FILE_HPP #include #include @@ -7,7 +7,7 @@ #include #include -#include +#include "bsafile.hpp" namespace Bsa { @@ -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/ba2file.hpp b/components/bsa/ba2file.hpp index 9a68d3afd0..0d51be4c0e 100644 --- a/components/bsa/ba2file.hpp +++ b/components/bsa/ba2file.hpp @@ -1,5 +1,5 @@ -#ifndef BSA_BA2_FILE_H -#define BSA_BA2_FILE_H +#ifndef OPENMW_COMPONENTS_BSA_BA2FILE_HPP +#define OPENMW_COMPONENTS_BSA_BA2FILE_HPP #include #include diff --git a/components/bsa/ba2gnrlfile.cpp b/components/bsa/ba2gnrlfile.cpp index 63dd3d1d50..f169440208 100644 --- a/components/bsa/ba2gnrlfile.cpp +++ b/components/bsa/ba2gnrlfile.cpp @@ -1,34 +1,20 @@ #include "ba2gnrlfile.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 -#pragma warning(pop) -#else -#include -#include -#include -#endif - -#include -#include -#include #include #include #include #include +#include "ba2file.hpp" +#include "memorystream.hpp" + namespace Bsa { // special marker for invalid records, @@ -221,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 6a212c5e81..080ba3a8df 100644 --- a/components/bsa/ba2gnrlfile.hpp +++ b/components/bsa/ba2gnrlfile.hpp @@ -1,12 +1,12 @@ -#ifndef BSA_BA2_GNRL_FILE_H -#define BSA_BA2_GNRL_FILE_H +#ifndef OPENMW_COMPONENTS_BSA_BA2GNRLFILE_HPP +#define OPENMW_COMPONENTS_BSA_BA2GNRLFILE_HPP #include #include #include #include -#include +#include "bsafile.hpp" namespace Bsa { @@ -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/bsa_file.cpp b/components/bsa/bsafile.cpp similarity index 99% rename from components/bsa/bsa_file.cpp rename to components/bsa/bsafile.cpp index 46639a729e..948b9dac8d 100644 --- a/components/bsa/bsa_file.cpp +++ b/components/bsa/bsafile.cpp @@ -4,7 +4,7 @@ Email: < korslund@gmail.com > WWW: https://openmw.org/ - This file (bsa_file.cpp) is part of the OpenMW package. + This file (bsafile.cpp) is part of the OpenMW package. OpenMW is distributed as free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -21,10 +21,7 @@ */ -#include "bsa_file.hpp" - -#include -#include +#include "bsafile.hpp" #include #include @@ -32,6 +29,9 @@ #include #include +#include +#include + using namespace Bsa; /// Error handling diff --git a/components/bsa/bsa_file.hpp b/components/bsa/bsafile.hpp similarity index 93% rename from components/bsa/bsa_file.hpp rename to components/bsa/bsafile.hpp index 03a0703885..7b910208d8 100644 --- a/components/bsa/bsa_file.hpp +++ b/components/bsa/bsafile.hpp @@ -4,7 +4,7 @@ Email: < korslund@gmail.com > WWW: https://openmw.org/ - This file (bsa_file.h) is part of the OpenMW package. + This file (bsafile.hpp) is part of the OpenMW package. OpenMW is distributed as free software: you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -21,8 +21,8 @@ */ -#ifndef BSA_BSA_FILE_H -#define BSA_BSA_FILE_H +#ifndef OPENMW_COMPONENTS_BSA_BSAFILE_HPP +#define OPENMW_COMPONENTS_BSA_BSAFILE_HPP #include #include @@ -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 14d90f5d91..655a4d2844 100644 --- a/components/bsa/compressedbsafile.cpp +++ b/components/bsa/compressedbsafile.cpp @@ -24,32 +24,20 @@ */ #include "compressedbsafile.hpp" +#include #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 #include +#include "memorystream.hpp" + namespace Bsa { /// Read header information from the input source @@ -290,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 8fa5c9a62a..1e359ea3fe 100644 --- a/components/bsa/compressedbsafile.hpp +++ b/components/bsa/compressedbsafile.hpp @@ -23,14 +23,14 @@ */ -#ifndef BSA_COMPRESSED_BSA_FILE_H -#define BSA_COMPRESSED_BSA_FILE_H +#ifndef OPENMW_COMPONENTS_BSA_COMPRESSEDBSAFILE_HPP +#define OPENMW_COMPONENTS_BSA_COMPRESSEDBSAFILE_HPP +#include #include #include -#include -#include +#include "bsafile.hpp" namespace Bsa { @@ -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/bsa/memorystream.hpp b/components/bsa/memorystream.hpp index 5662dde8ff..945d0b33d9 100644 --- a/components/bsa/memorystream.hpp +++ b/components/bsa/memorystream.hpp @@ -23,13 +23,14 @@ */ -#ifndef BSA_MEMORY_STREAM_H -#define BSA_MEMORY_STREAM_H +#ifndef OPENMW_COMPONENTS_BSA_MEMORYSTREAM_HPP +#define OPENMW_COMPONENTS_BSA_MEMORYSTREAM_HPP -#include #include #include +#include + namespace Bsa { /** 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/contentselector/model/contentmodel.cpp b/components/contentselector/model/contentmodel.cpp index 2604a02885..53d5476fb6 100644 --- a/components/contentselector/model/contentmodel.cpp +++ b/components/contentselector/model/contentmodel.cpp @@ -129,7 +129,7 @@ Qt::ItemFlags ContentSelectorModel::ContentModel::flags(const QModelIndex& index { if (depFile->isGameFile() && file->gameFiles().contains(depFile->fileName(), Qt::CaseInsensitive)) { - if (!depFile->builtIn() && !depFile->fromAnotherConfigFile() && !mCheckedFiles.contains(depFile)) + if (!isChecked(depFile)) break; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; @@ -215,8 +215,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int if (file == mGameFile) return QVariant(); - return (file->builtIn() || file->fromAnotherConfigFile() || mCheckedFiles.contains(file)) ? Qt::Checked - : Qt::Unchecked; + return isChecked(file) ? Qt::Checked : Qt::Unchecked; } case Qt::UserRole: @@ -230,7 +229,7 @@ QVariant ContentSelectorModel::ContentModel::data(const QModelIndex& index, int } case Qt::UserRole + 1: - return mCheckedFiles.contains(file); + return isChecked(file); } return QVariant(); } @@ -264,9 +263,9 @@ bool ContentSelectorModel::ContentModel::setData(const QModelIndex& index, const { int checkValue = value.toInt(); if (checkValue == Qt::Checked) - return mCheckedFiles.contains(file) || setCheckState(file, true); + return isChecked(file) || setCheckState(file, true); if (checkValue == Qt::Unchecked) - return !mCheckedFiles.contains(file) || setCheckState(file, false); + return !isChecked(file) || setCheckState(file, false); } } @@ -606,6 +605,14 @@ void ContentSelectorModel::ContentModel::sortFiles() emit layoutChanged(); } +bool ContentSelectorModel::ContentModel::isChecked(const EsmFile* file) const +{ + if (file == nullptr) + return false; + + return file->builtIn() || file->fromAnotherConfigFile() || mCheckedFiles.contains(file); +} + bool ContentSelectorModel::ContentModel::isEnabled(const QModelIndex& index) const { return (flags(index) & Qt::ItemIsEnabled); @@ -691,7 +698,7 @@ QList ContentSelectorModel::ContentModel:: } else { - if (!mCheckedFiles.contains(dependentFile)) + if (!isChecked(dependentFile)) { errors.append(LoadOrderError(LoadOrderError::ErrorCode_InactiveDependency, dependentfileName)); } @@ -706,7 +713,7 @@ QList ContentSelectorModel::ContentModel:: { // Warn the user if Bloodmoon is loaded before Tribunal (Tribunal is not a hard dependency) const EsmFile* tribunalFile = item("Tribunal.esm"); - if (tribunalFile != nullptr && mCheckedFiles.contains(tribunalFile) && row < indexFromItem(tribunalFile).row()) + if (isChecked(tribunalFile) && row < indexFromItem(tribunalFile).row()) errors.append(LoadOrderError(LoadOrderError::ErrorCode_LoadOrder, "Tribunal.esm")); } @@ -770,8 +777,9 @@ bool ContentSelectorModel::ContentModel::setCheckState(const EsmFile* file, bool for (const QString& upstreamName : file->gameFiles()) { const EsmFile* upstreamFile = item(upstreamName); - if (upstreamFile == nullptr || !mCheckedFiles.insert(upstreamFile).second) + if (upstreamFile == nullptr || isChecked(upstreamFile)) continue; + mCheckedFiles.insert(upstreamFile); QModelIndex upstreamIndex = indexFromItem(upstreamFile); emit dataChanged(upstreamIndex, upstreamIndex); } diff --git a/components/contentselector/model/contentmodel.hpp b/components/contentselector/model/contentmodel.hpp index cf182263f9..6260e5f1fe 100644 --- a/components/contentselector/model/contentmodel.hpp +++ b/components/contentselector/model/contentmodel.hpp @@ -58,6 +58,7 @@ namespace ContentSelectorModel QStringList gameFiles() const; void setCurrentGameFile(const EsmFile* file); + bool isChecked(const EsmFile* file) const; bool isEnabled(const QModelIndex& index) const; bool setCheckState(const EsmFile* file, bool isChecked); bool isNew(const QString& filepath) const; diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index f8374b5db5..5fd54ee789 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -157,7 +157,7 @@ void ContentSelectorView::ContentSelector::setGameFile(const QString& filename) index = ui->gameFileView->findText(file->fileName()); // verify that the current index is also checked in the model - if (!mContentModel->setCheckState(file, true)) + if (!mContentModel->isChecked(file) && !mContentModel->setCheckState(file, true)) { // throw error in case file not found? return; diff --git a/components/crashcatcher/crashcatcher.hpp b/components/crashcatcher/crashcatcher.hpp index 16b416cf98..60f749e57a 100644 --- a/components/crashcatcher/crashcatcher.hpp +++ b/components/crashcatcher/crashcatcher.hpp @@ -1,5 +1,5 @@ -#ifndef CRASHCATCHER_H -#define CRASHCATCHER_H +#ifndef OPENMW_COMPONENTS_CRASHCATCHER_CRASHCATCHER_HPP +#define OPENMW_COMPONENTS_CRASHCATCHER_CRASHCATCHER_HPP #include diff --git a/components/crashcatcher/windows_crashcatcher.cpp b/components/crashcatcher/windowscrashcatcher.cpp similarity index 98% rename from components/crashcatcher/windows_crashcatcher.cpp rename to components/crashcatcher/windowscrashcatcher.cpp index bd5e94a6aa..c89ab9c335 100644 --- a/components/crashcatcher/windows_crashcatcher.cpp +++ b/components/crashcatcher/windowscrashcatcher.cpp @@ -1,17 +1,18 @@ -#include "windows_crashcatcher.hpp" +#include "windowscrashcatcher.hpp" #include #include #include #include -#include "windows_crashmonitor.hpp" -#include "windows_crashshm.hpp" -#include "windowscrashdumppathhelpers.hpp" #include #include +#include "windowscrashdumppathhelpers.hpp" +#include "windowscrashmonitor.hpp" +#include "windowscrashshm.hpp" + namespace Crash { namespace diff --git a/components/crashcatcher/windows_crashcatcher.hpp b/components/crashcatcher/windowscrashcatcher.hpp similarity index 94% rename from components/crashcatcher/windows_crashcatcher.hpp rename to components/crashcatcher/windowscrashcatcher.hpp index 5bc78b19fa..0e550d2c9b 100644 --- a/components/crashcatcher/windows_crashcatcher.hpp +++ b/components/crashcatcher/windowscrashcatcher.hpp @@ -1,11 +1,12 @@ -#ifndef WINDOWS_CRASHCATCHER_HPP -#define WINDOWS_CRASHCATCHER_HPP +#ifndef OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHCATCHER_HPP +#define OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHCATCHER_HPP #include -#include #include +#include "crashcatcher.hpp" + namespace Crash { @@ -80,4 +81,4 @@ namespace Crash } // namespace Crash -#endif // WINDOWS_CRASHCATCHER_HPP +#endif diff --git a/components/crashcatcher/windowscrashdumppathhelpers.hpp b/components/crashcatcher/windowscrashdumppathhelpers.hpp index fa64969301..9e11c57834 100644 --- a/components/crashcatcher/windowscrashdumppathhelpers.hpp +++ b/components/crashcatcher/windowscrashdumppathhelpers.hpp @@ -1,8 +1,10 @@ -#ifndef COMPONENTS_CRASH_WINDOWSCRASHDUMPPATHHELPERS_H -#include "windows_crashshm.hpp" +#ifndef OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHDUMPPATHHELPERS_HPP +#define OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHDUMPPATHHELPERS_HPP #include +#include "windowscrashshm.hpp" + namespace Crash { std::filesystem::path getCrashDumpPath(const CrashSHM& crashShm); diff --git a/components/crashcatcher/windows_crashmonitor.cpp b/components/crashcatcher/windowscrashmonitor.cpp similarity index 99% rename from components/crashcatcher/windows_crashmonitor.cpp rename to components/crashcatcher/windowscrashmonitor.cpp index 116a678fef..de66e20b61 100644 --- a/components/crashcatcher/windows_crashmonitor.cpp +++ b/components/crashcatcher/windowscrashmonitor.cpp @@ -1,4 +1,4 @@ -#include "windows_crashmonitor.hpp" +#include "windowscrashmonitor.hpp" #include #include @@ -10,12 +10,13 @@ #include -#include "windows_crashcatcher.hpp" -#include "windows_crashshm.hpp" -#include "windowscrashdumppathhelpers.hpp" #include #include +#include "windowscrashcatcher.hpp" +#include "windowscrashdumppathhelpers.hpp" +#include "windowscrashshm.hpp" + namespace Crash { std::unordered_map CrashMonitor::smEventHookOwners{}; diff --git a/components/crashcatcher/windows_crashmonitor.hpp b/components/crashcatcher/windowscrashmonitor.hpp similarity index 90% rename from components/crashcatcher/windows_crashmonitor.hpp rename to components/crashcatcher/windowscrashmonitor.hpp index 25ee710fd3..6dfd5cabc6 100644 --- a/components/crashcatcher/windows_crashmonitor.hpp +++ b/components/crashcatcher/windowscrashmonitor.hpp @@ -1,5 +1,5 @@ -#ifndef WINDOWS_CRASHMONITOR_HPP -#define WINDOWS_CRASHMONITOR_HPP +#ifndef OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHMONITOR_HPP +#define OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHMONITOR_HPP #include @@ -60,4 +60,4 @@ namespace Crash } // namespace Crash -#endif // WINDOWS_CRASHMONITOR_HPP +#endif diff --git a/components/crashcatcher/windows_crashshm.hpp b/components/crashcatcher/windowscrashshm.hpp similarity index 90% rename from components/crashcatcher/windows_crashshm.hpp rename to components/crashcatcher/windowscrashshm.hpp index 2cad54b17a..6ac09234ba 100644 --- a/components/crashcatcher/windows_crashshm.hpp +++ b/components/crashcatcher/windowscrashshm.hpp @@ -1,5 +1,5 @@ -#ifndef WINDOWS_CRASHSHM_HPP -#define WINDOWS_CRASHSHM_HPP +#ifndef OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHSHM_HPP +#define OPENMW_COMPONENTS_CRASHCATCHER_WINDOWSCRASHSHM_HPP #include @@ -55,4 +55,4 @@ namespace Crash } // namespace Crash -#endif // WINDOWS_CRASHSHM_HPP +#endif diff --git a/components/debug/debugging.cpp b/components/debug/debugging.cpp index 2ba7b2072e..4cb625c548 100644 --- a/components/debug/debugging.cpp +++ b/components/debug/debugging.cpp @@ -22,7 +22,7 @@ #include #ifdef _WIN32 -#include +#include #include #include 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/esmreader.hpp b/components/esm3/esmreader.hpp index 5af5e75573..9bae5f217e 100644 --- a/components/esm3/esmreader.hpp +++ b/components/esm3/esmreader.hpp @@ -10,7 +10,7 @@ #include #include -#include +#include #include "components/esm/decompose.hpp" #include "components/esm/esmcommon.hpp" diff --git a/components/esm3/esmwriter.cpp b/components/esm3/esmwriter.cpp index 47c861e3ca..8bae844585 100644 --- a/components/esm3/esmwriter.cpp +++ b/components/esm3/esmwriter.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include "formatversion.hpp" diff --git a/components/esm3/formatversion.hpp b/components/esm3/formatversion.hpp index 32e245d7d1..c205f2fbb7 100644 --- a/components/esm3/formatversion.hpp +++ b/components/esm3/formatversion.hpp @@ -32,7 +32,8 @@ namespace ESM inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 5; inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; - inline constexpr FormatVersion OpenMW0_49SaveGameFormatVersion = CurrentSaveGameFormatVersion; + inline constexpr FormatVersion OpenMW0_49SaveGameFormatVersion = 34; + inline constexpr FormatVersion OpenMW0_50SaveGameFormatVersion = CurrentSaveGameFormatVersion; } #endif 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/esm4/reader.cpp b/components/esm4/reader.cpp index 505922601d..43a9e26418 100644 --- a/components/esm4/reader.cpp +++ b/components/esm4/reader.cpp @@ -40,7 +40,7 @@ #include #include #include -#include +#include #include #include "grouptype.hpp" 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.hpp b/components/files/configurationmanager.hpp index 306ea38fe1..2e10f21252 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 diff --git a/components/files/linuxpath.cpp b/components/files/linuxpath.cpp index 4dbe119917..2e74948fff 100644 --- a/components/files/linuxpath.cpp +++ b/components/files/linuxpath.cpp @@ -7,7 +7,6 @@ #include #include -#include #include namespace @@ -51,14 +50,6 @@ namespace Files LinuxPath::LinuxPath(const std::string& application_name) : mName(application_name) { - std::error_code ec; - current_path(getLocalPath(), ec); - const auto err = ec.value(); - if (err != 0) - { - Log(Debug::Warning) << "Error " << err << " " << std::generic_category().message(errno) - << " when changing current directory"; - } } std::filesystem::path LinuxPath::getUserConfigPath() const diff --git a/components/files/macospath.cpp b/components/files/macospath.cpp index 4b37c2fb26..191f3b15a6 100644 --- a/components/files/macospath.cpp +++ b/components/files/macospath.cpp @@ -64,11 +64,6 @@ namespace Files MacOsPath::MacOsPath(const std::string& application_name) : mName(application_name) { - std::filesystem::path binary_path = getBinaryPath(); - std::error_code ec; - std::filesystem::current_path(binary_path.parent_path(), ec); - if (ec.value() != 0) - Log(Debug::Warning) << "Error " << ec.message() << " when changing current directory"; } std::filesystem::path MacOsPath::getUserConfigPath() const @@ -102,7 +97,7 @@ namespace Files std::filesystem::path MacOsPath::getLocalPath() const { - return std::filesystem::path("../Resources/"); + return getBinaryPath().parent_path().parent_path() / "Resources"; } std::filesystem::path MacOsPath::getGlobalDataPath() const diff --git a/components/files/windowspath.cpp b/components/files/windowspath.cpp index 6fb9845976..77faa23131 100644 --- a/components/files/windowspath.cpp +++ b/components/files/windowspath.cpp @@ -26,10 +26,6 @@ namespace Files WindowsPath::WindowsPath(const std::string& application_name) : mName(application_name) { - std::error_code ec; - current_path(getLocalPath(), ec); - if (ec.value() != 0) - Log(Debug::Warning) << "Error " << ec.value() << " when changing current directory"; } std::filesystem::path WindowsPath::getUserConfigPath() const 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/fontloader/fontloader.hpp b/components/fontloader/fontloader.hpp index a7269f1a56..d0511659a0 100644 --- a/components/fontloader/fontloader.hpp +++ b/components/fontloader/fontloader.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include namespace VFS { diff --git a/components/fx/lexer.cpp b/components/fx/lexer.cpp index 6140c7375c..cab59df314 100644 --- a/components/fx/lexer.cpp +++ b/components/fx/lexer.cpp @@ -6,10 +6,9 @@ #include #include -#include #include -namespace fx +namespace Fx { namespace Lexer { diff --git a/components/fx/lexer.hpp b/components/fx/lexer.hpp index fc7d4ec9d7..1b32298608 100644 --- a/components/fx/lexer.hpp +++ b/components/fx/lexer.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_LEXER_H -#define OPENMW_COMPONENTS_FX_LEXER_H +#ifndef OPENMW_COMPONENTS_FX_LEXER_HPP +#define OPENMW_COMPONENTS_FX_LEXER_HPP #include #include @@ -7,9 +7,9 @@ #include #include -#include "lexer_types.hpp" +#include "lexertypes.hpp" -namespace fx +namespace Fx { namespace Lexer { diff --git a/components/fx/lexer_types.hpp b/components/fx/lexertypes.hpp similarity index 97% rename from components/fx/lexer_types.hpp rename to components/fx/lexertypes.hpp index 9fe13ac827..327d6832c8 100644 --- a/components/fx/lexer_types.hpp +++ b/components/fx/lexertypes.hpp @@ -1,10 +1,10 @@ -#ifndef OPENMW_COMPONENTS_FX_LEXER_TYPES_H -#define OPENMW_COMPONENTS_FX_LEXER_TYPES_H +#ifndef OPENMW_COMPONENTS_FX_LEXERTYPES_HPP +#define OPENMW_COMPONENTS_FX_LEXERTYPES_HPP #include #include -namespace fx +namespace Fx { namespace Lexer { @@ -165,4 +165,4 @@ namespace fx } } -#endif \ No newline at end of file +#endif diff --git a/components/fx/parse_constants.hpp b/components/fx/parseconstants.hpp similarity index 96% rename from components/fx/parse_constants.hpp rename to components/fx/parseconstants.hpp index 2057476f3e..eec315042b 100644 --- a/components/fx/parse_constants.hpp +++ b/components/fx/parseconstants.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_PARSE_CONSTANTS_H -#define OPENMW_COMPONENTS_FX_PARSE_CONSTANTS_H +#ifndef OPENMW_COMPONENTS_FX_PARSECONSTANTS_HPP +#define OPENMW_COMPONENTS_FX_PARSECONSTANTS_HPP #include #include @@ -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 e176afc699..2e68bddcc5 100644 --- a/components/fx/pass.hpp +++ b/components/fx/pass.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_PASS_H -#define OPENMW_COMPONENTS_FX_PASS_H +#ifndef OPENMW_COMPONENTS_FX_PASS_HPP +#define OPENMW_COMPONENTS_FX_PASS_HPP #include #include @@ -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 aaaf56e18a..5865298fe5 100644 --- a/components/fx/technique.cpp +++ b/components/fx/technique.cpp @@ -17,7 +17,7 @@ #include #include -#include "parse_constants.hpp" +#include "parseconstants.hpp" namespace { @@ -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; @@ -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 2778763a9a..ebf3fe30f5 100644 --- a/components/fx/technique.hpp +++ b/components/fx/technique.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_TECHNIQUE_H -#define OPENMW_COMPONENTS_FX_TECHNIQUE_H +#ifndef OPENMW_COMPONENTS_FX_TECHNIQUE_HPP +#define OPENMW_COMPONENTS_FX_TECHNIQUE_HPP #include #include @@ -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 596b54c217..ddfa966010 100644 --- a/components/fx/types.hpp +++ b/components/fx/types.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_TYPES_H -#define OPENMW_COMPONENTS_FX_TYPES_H +#ifndef OPENMW_COMPONENTS_FX_TYPES_HPP +#define OPENMW_COMPONENTS_FX_TYPES_HPP #include #include @@ -12,7 +12,7 @@ #include #include -namespace fx +namespace Fx { namespace Types { 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 6217af7fee..c91fa01c4e 100644 --- a/components/fx/widgets.hpp +++ b/components/fx/widgets.hpp @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_FX_WIDGETS_H -#define OPENMW_COMPONENTS_FX_WIDGETS_H +#ifndef OPENMW_COMPONENTS_FX_WIDGETS_HPP +#define OPENMW_COMPONENTS_FX_WIDGETS_HPP #include #include @@ -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..27a4d603d4 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) diff --git a/components/l10n/manager.hpp b/components/l10n/manager.hpp index 2ee54921b3..4b047fa9d7 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 diff --git a/components/l10n/messagebundles.cpp b/components/l10n/messagebundles.cpp index 2948ff155e..e8a56a9bb3 100644 --- a/components/l10n/messagebundles.cpp +++ b/components/l10n/messagebundles.cpp @@ -7,7 +7,7 @@ #include -namespace l10n +namespace L10n { MessageBundles::MessageBundles(const std::vector& preferredLocales, icu::Locale& fallbackLocale) : mFallbackLocale(fallbackLocale) diff --git a/components/l10n/messagebundles.hpp b/components/l10n/messagebundles.hpp index 0ea92e93fe..f142d3f2ac 100644 --- a/components/l10n/messagebundles.hpp +++ b/components/l10n/messagebundles.hpp @@ -10,7 +10,7 @@ #include #include -namespace l10n +namespace L10n { /** * @brief A collection of Message Bundles diff --git a/components/l10n/qttranslations.cpp b/components/l10n/qttranslations.cpp index 9bc146699e..1051b3dd2d 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{}; 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..8153efd5b5 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"); 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/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/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/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 604cf92c33..e32ef76d95 100644 --- a/components/nif/nifkey.hpp +++ b/components/nif/nifkey.hpp @@ -3,7 +3,8 @@ #ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP #define OPENMW_COMPONENTS_NIF_NIFKEY_HPP -#include +#include +#include #include "exception.hpp" #include "niffile.hpp" @@ -17,7 +18,7 @@ namespace Nif InterpolationType_Unknown = 0, InterpolationType_Linear = 1, InterpolationType_Quadratic = 2, - InterpolationType_TBC = 3, + InterpolationType_TCB = 3, InterpolationType_XYZ = 4, InterpolationType_Constant = 5 }; @@ -28,23 +29,25 @@ namespace Nif T mValue; T mInTan; // Only for Quadratic interpolation, and never for QuaternionKeyList T mOutTan; // Only for Quadratic interpolation, and never for QuaternionKeyList - - // FIXME: Implement TBC interpolation - /* - float mTension; // Only for TBC interpolation - float mBias; // Only for TBC interpolation - float mContinuity; // Only for TBC interpolation - */ }; - using FloatKey = KeyT; - using Vector3Key = KeyT; - using Vector4Key = KeyT; - using QuaternionKey = KeyT; + + template + struct TCBKey + { + float mTime; + T mValue{}; + T mInTan{}; + T mOutTan{}; + float mTension; + float mContinuity; + float mBias; + }; 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; @@ -76,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 = {}; @@ -88,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) @@ -98,18 +105,24 @@ namespace Nif float time; nif->read(time); readQuadratic(*nif, key); - mKeys[time] = key; + mKeys.emplace_back(time, key); } } - else if (mInterpolationType == InterpolationType_TBC) + else if (mInterpolationType == InterpolationType_TCB) { - for (size_t i = 0; i < count; i++) + std::vector> tcbKeys(count); + for (TCBKey& tcbKey : tcbKeys) { - float time; - nif->read(time); - readTBC(*nif, key); - mKeys[time] = key; + nif->read(tcbKey.mTime); + tcbKey.mValue = ((*nif).*getValue)(); + nif->read(tcbKey.mTension); + nif->read(tcbKey.mContinuity); + nif->read(tcbKey.mBias); } + generateTCBTangents(tcbKeys); + 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) { @@ -125,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: @@ -140,14 +155,43 @@ namespace Nif static void readQuadratic(NIFStream& nif, KeyT& key) { readValue(nif, key); } - static void readTBC(NIFStream& nif, KeyT& key) + template + static void generateTCBTangents(std::vector>& keys) { - readValue(nif, key); - /*key.mTension = */ nif.get(); - /*key.mBias = */ nif.get(); - /*key.mContinuity = */ nif.get(); + if (keys.size() <= 1) + return; + + for (std::size_t i = 0; i < keys.size(); ++i) + { + TCBKey& curr = keys[i]; + 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 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); + } + } + + static void generateTCBTangents(std::vector>& keys) + { + // TODO: is this even legal? + } + + static void generateTCBTangents(std::vector>& keys) + { + // TODO: implement TCB interpolation for quaternions } }; + using FloatKeyMap = KeyMapT>; using Vector3KeyMap = KeyMapT>; using Vector4KeyMap = KeyMapT>; diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index ef63bae937..6c361401e8 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -2,9 +2,9 @@ #include -#include "niffile.hpp" +#include -#include "../to_utf8/to_utf8.hpp" +#include "niffile.hpp" namespace { 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 99d3df9545..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(); } @@ -131,6 +137,7 @@ namespace NifOsg case Nif::InterpolationType_Constant: return fraction > 0.5f ? b.mValue : a.mValue; case Nif::InterpolationType_Quadratic: + case Nif::InterpolationType_TCB: { // Using a cubic Hermite spline. // b1(t) = 2t^3 - 3t^2 + 1 @@ -147,7 +154,6 @@ namespace NifOsg const float b4 = t3 - t2; return a.mValue * b1 + b.mValue * b2 + a.mOutTan * b3 + b.mInTan * b4; } - // TODO: Implement TBC interpolation default: return a.mValue + ((b.mValue - a.mValue) * fraction); } @@ -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/platform/file.posix.cpp b/components/platform/fileposix.cpp similarity index 100% rename from components/platform/file.posix.cpp rename to components/platform/fileposix.cpp diff --git a/components/platform/file.stdio.cpp b/components/platform/filestdio.cpp similarity index 100% rename from components/platform/file.stdio.cpp rename to components/platform/filestdio.cpp diff --git a/components/platform/file.win32.cpp b/components/platform/filewin32.cpp similarity index 100% rename from components/platform/file.win32.cpp rename to components/platform/filewin32.cpp diff --git a/components/process/processinvoker.cpp b/components/process/processinvoker.cpp index 9489076acb..f1af67d20d 100644 --- a/components/process/processinvoker.cpp +++ b/components/process/processinvoker.cpp @@ -1,14 +1,11 @@ #include "processinvoker.hpp" +#include #include #include #include #include -#if defined(Q_OS_MAC) -#include -#endif - Process::ProcessInvoker::ProcessInvoker(QObject* parent) : QObject(parent) { @@ -60,12 +57,9 @@ bool Process::ProcessInvoker::startProcess(const QString& name, const QStringLis QString path(name); #ifdef Q_OS_WIN path.append(QLatin1String(".exe")); -#elif defined(Q_OS_MAC) - QDir dir(QCoreApplication::applicationDirPath()); - path = dir.absoluteFilePath(name); -#else - path.prepend(QLatin1String("./")); #endif + QDir dir(QCoreApplication::applicationDirPath()); + path = dir.absoluteFilePath(path); QFileInfo info(path); 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/gl4es_init.cpp b/components/sdlutil/gl4esinit.cpp similarity index 97% rename from components/sdlutil/gl4es_init.cpp rename to components/sdlutil/gl4esinit.cpp index 044b54ce02..13015dee82 100644 --- a/components/sdlutil/gl4es_init.cpp +++ b/components/sdlutil/gl4esinit.cpp @@ -1,7 +1,7 @@ // EGL does not work reliably for feature detection. // Instead, we initialize gl4es manually. #ifdef OPENMW_GL4ES_MANUAL_INIT -#include "gl4es_init.h" +#include "gl4esinit.h" // For glHint #include diff --git a/components/sdlutil/gl4es_init.h b/components/sdlutil/gl4esinit.h similarity index 67% rename from components/sdlutil/gl4es_init.h rename to components/sdlutil/gl4esinit.h index 6a19d3dfe5..8916d50a7e 100644 --- a/components/sdlutil/gl4es_init.h +++ b/components/sdlutil/gl4esinit.h @@ -1,5 +1,5 @@ -#ifndef OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H -#define OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H +#ifndef OPENMW_COMPONENTS_SDLUTIL_GL4ESINIT_H +#define OPENMW_COMPONENTS_SDLUTIL_GL4ESINIT_H #ifdef OPENMW_GL4ES_MANUAL_INIT #include @@ -10,4 +10,4 @@ extern "C" void openmw_gl4es_init(SDL_Window* window); #endif // OPENMW_GL4ES_MANUAL_INIT -#endif // OPENMW_COMPONENTS_SDLUTIL_GL4ES_INIT_H +#endif // OPENMW_COMPONENTS_SDLUTIL_GL4ESINIT_H diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index 36947460df..9ff0545930 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -3,7 +3,7 @@ #include #ifdef OPENMW_GL4ES_MANUAL_INIT -#include "gl4es_init.h" +#include "gl4esinit.h" #endif namespace SDLUtil 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/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/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index d97276576f..0e6b5e79c6 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -287,11 +287,17 @@ namespace Shader addedState->setName("addedState"); } + // This list is used both for detecting known texture types (including added normal maps etc.) and setting the + // shader defines. Normal maps and normal height maps both get sent to the shader as a normal map, so the latter + // must be detected separately. const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", "specularMap", "decalMap", "bumpMap", "glossMap" }; bool isTextureNameRecognized(std::string_view name) { - return std::find(std::begin(defaultTextures), std::end(defaultTextures), name) != std::end(defaultTextures); + if (std::find(std::begin(defaultTextures), std::end(defaultTextures), name) != std::end(defaultTextures)) + return true; + else + return name == "normalHeightMap"; } void ShaderVisitor::applyStateSet(osg::ref_ptr stateset, osg::Node& node) @@ -439,8 +445,9 @@ namespace Shader if (!writableStateSet) writableStateSet = getWritableStateSet(node); writableStateSet->setTextureAttributeAndModes(unit, normalMapTex, osg::StateAttribute::ON); - writableStateSet->setTextureAttributeAndModes( - unit, new SceneUtil::TextureType("normalMap"), osg::StateAttribute::ON); + writableStateSet->setTextureAttributeAndModes(unit, + new SceneUtil::TextureType(normalHeight ? "normalHeightMap" : "normalMap"), + osg::StateAttribute::ON); mRequirements.back().mTextures[unit] = "normalMap"; mRequirements.back().mTexStageRequiringTangents = unit; mRequirements.back().mShaderRequired = true; 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/to_utf8/.gitignore b/components/to_utf8/.gitignore deleted file mode 100644 index 4e0357749e..0000000000 --- a/components/to_utf8/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gen_iconv diff --git a/components/to_utf8/Makefile b/components/to_utf8/Makefile deleted file mode 100644 index 5234d455ae..0000000000 --- a/components/to_utf8/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -tables_gen.hpp: gen_iconv - ./gen_iconv > tables_gen.hpp - -gen_iconv: gen_iconv.cpp - g++ -Wall $^ -o $@ - -clean: - rm -f ./gen_iconv \ No newline at end of file diff --git a/components/toutf8/.gitignore b/components/toutf8/.gitignore new file mode 100644 index 0000000000..f1f54f5aea --- /dev/null +++ b/components/toutf8/.gitignore @@ -0,0 +1 @@ +geniconv diff --git a/components/toutf8/Makefile b/components/toutf8/Makefile new file mode 100644 index 0000000000..e46176d3ed --- /dev/null +++ b/components/toutf8/Makefile @@ -0,0 +1,8 @@ +tablesgen.hpp: geniconv + ./geniconv > tablesgen.hpp + +geniconv: geniconv.cpp + g++ -Wall $^ -o $@ + +clean: + rm -f ./geniconv diff --git a/components/to_utf8/gen_iconv.cpp b/components/toutf8/geniconv.cpp similarity index 95% rename from components/to_utf8/gen_iconv.cpp rename to components/toutf8/geniconv.cpp index 062b1173fc..48a7fb4627 100644 --- a/components/to_utf8/gen_iconv.cpp +++ b/components/toutf8/geniconv.cpp @@ -1,4 +1,4 @@ -// This program generates the file tables_gen.hpp +// This program generates the file tablesgen.hpp #include @@ -88,7 +88,7 @@ int write_table(const std::string& charset, const std::string& tableName) int main() { // Write header guard - std::cout << "#ifndef COMPONENTS_TOUTF8_TABLE_GEN_H\n#define COMPONENTS_TOUTF8_TABLE_GEN_H\n\n"; + std::cout << "#ifndef OPENMW_COMPONENTS_TOUTF8_TABLESGEN_HPP\n#define OPENMW_COMPONENTS_TOUTF8_TABLESGEN_HPP\n\n"; // Write namespace std::cout << "namespace ToUTF8\n{\n\n"; diff --git a/components/to_utf8/tables_gen.hpp b/components/toutf8/tablesgen.hpp similarity index 99% rename from components/to_utf8/tables_gen.hpp rename to components/toutf8/tablesgen.hpp index b25bb54bab..9ef5097ece 100644 --- a/components/to_utf8/tables_gen.hpp +++ b/components/toutf8/tablesgen.hpp @@ -1,5 +1,5 @@ -#ifndef COMPONENTS_TOUTF8_TABLE_GEN_H -#define COMPONENTS_TOUTF8_TABLE_GEN_H +#ifndef OPENMW_COMPONENTS_TOUTF8_TABLESGEN_HPP +#define OPENMW_COMPONENTS_TOUTF8_TABLESGEN_HPP namespace ToUTF8 { diff --git a/components/to_utf8/to_utf8.cpp b/components/toutf8/toutf8.cpp similarity index 99% rename from components/to_utf8/to_utf8.cpp rename to components/toutf8/toutf8.cpp index 7e34147b1c..03b12042e4 100644 --- a/components/to_utf8/to_utf8.cpp +++ b/components/toutf8/toutf8.cpp @@ -1,4 +1,4 @@ -#include "to_utf8.hpp" +#include "toutf8.hpp" #include #include @@ -41,7 +41,7 @@ */ // Generated tables -#include "tables_gen.hpp" +#include "tablesgen.hpp" using namespace ToUTF8; diff --git a/components/to_utf8/to_utf8.hpp b/components/toutf8/toutf8.hpp similarity index 97% rename from components/to_utf8/to_utf8.hpp rename to components/toutf8/toutf8.hpp index 0dde0fced6..e258a0e94b 100644 --- a/components/to_utf8/to_utf8.hpp +++ b/components/toutf8/toutf8.hpp @@ -1,5 +1,5 @@ -#ifndef COMPONENTS_TOUTF8_H -#define COMPONENTS_TOUTF8_H +#ifndef OPENMW_COMPONENTS_TOUTF8_TOUTF8_HPP +#define OPENMW_COMPONENTS_TOUTF8_TOUTF8_HPP #include #include diff --git a/components/translation/translation.hpp b/components/translation/translation.hpp index a7b6087ac7..18e40713e5 100644 --- a/components/translation/translation.hpp +++ b/components/translation/translation.hpp @@ -2,7 +2,7 @@ #define COMPONENTS_TRANSLATION_DATA_H #include -#include +#include namespace Translation { diff --git a/components/vfs/bsaarchive.hpp b/components/vfs/bsaarchive.hpp index 664466fa40..f89e47d971 100644 --- a/components/vfs/bsaarchive.hpp +++ b/components/vfs/bsaarchive.hpp @@ -7,48 +7,75 @@ #include #include -#include +#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/components/widgets/list.cpp b/components/widgets/list.cpp index 896057443c..416590ed48 100644 --- a/components/widgets/list.cpp +++ b/components/widgets/list.cpp @@ -29,14 +29,14 @@ namespace Gui MyGUI::Align::Top | MyGUI::Align::Left | MyGUI::Align::Stretch, getName() + "_ScrollView"); } - void MWList::addItem(std::string_view name) + void MWList::addItem(std::string_view name, int verticalPadding) { - mItems.emplace_back(name); + mItems.emplace_back(name, verticalPadding); } void MWList::addSeparator() { - mItems.emplace_back(std::string{}); + addItem({}); } void MWList::adjustSize() @@ -48,7 +48,6 @@ namespace Gui { constexpr int _scrollBarWidth = 20; // fetch this from skin? const int scrollBarWidth = scrollbarShown ? _scrollBarWidth : 0; - constexpr int spacing = 3; int viewPosition = -mScrollView->getViewOffset().top; while (mScrollView->getChildCount()) @@ -60,25 +59,27 @@ namespace Gui int i = 0; for (const auto& item : mItems) { - if (!item.empty()) + mItemHeight += item.mVPadding; + if (!item.mName.empty()) { if (mListItemSkin.empty()) return; MyGUI::Button* button = mScrollView->createWidget(mListItemSkin, MyGUI::IntCoord(0, mItemHeight, mScrollView->getSize().width - scrollBarWidth - 2, 24), - MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + item); - button->setCaption(item); + MyGUI::Align::Left | MyGUI::Align::Top, getName() + "_item_" + item.mName); + button->setCaption(item.mName); button->getSubWidgetText()->setWordWrap(true); button->getSubWidgetText()->setTextAlign(MyGUI::Align::Left); button->eventMouseWheel += MyGUI::newDelegate(this, &MWList::onMouseWheelMoved); button->eventMouseButtonClick += MyGUI::newDelegate(this, &MWList::onItemSelected); button->setNeedKeyFocus(true); - int height = button->getTextSize().height; + // Morrowind list item text widgets are typically 18 pixels tall + int height = button->getTextSize().height + 2; button->setSize(MyGUI::IntSize(button->getSize().width, height)); button->setUserData(i); - mItemHeight += height + spacing; + mItemHeight += height; } else { @@ -87,8 +88,9 @@ namespace Gui MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); separator->setNeedMouseFocus(false); - mItemHeight += 18 + spacing; + mItemHeight += 18; } + mItemHeight += item.mVPadding; ++i; } @@ -123,18 +125,19 @@ namespace Gui const std::string& MWList::getItemNameAt(size_t at) { assert(at < mItems.size() && "List item out of bounds"); - return mItems[at]; + return mItems[at].mName; } void MWList::sort() { // A special case for separators is not needed for now - std::sort(mItems.begin(), mItems.end(), Misc::StringUtils::ciLess); + std::sort(mItems.begin(), mItems.end(), + [](const auto& left, const auto& right) { return Misc::StringUtils::ciLess(left.mName, right.mName); }); } void MWList::removeItem(const std::string& name) { - auto it = std::find(mItems.begin(), mItems.end(), name); + auto it = std::find_if(mItems.begin(), mItems.end(), [&name](const auto& item) { return item.mName == name; }); assert(it != mItems.end()); mItems.erase(it); } diff --git a/components/widgets/list.hpp b/components/widgets/list.hpp index 3d5e320cf7..f67a7da97b 100644 --- a/components/widgets/list.hpp +++ b/components/widgets/list.hpp @@ -36,7 +36,7 @@ namespace Gui void adjustSize(); void sort(); - void addItem(std::string_view name); + void addItem(std::string_view name, int verticalPadding = 0); void addSeparator(); ///< add a seperator between the current and the next item. void removeItem(const std::string& name); size_t getItemCount(); @@ -64,7 +64,18 @@ namespace Gui MyGUI::Widget* mClient; std::string mListItemSkin; - std::vector mItems; + struct ListItemData + { + std::string mName; + int mVPadding; + + ListItemData(std::string_view name, int verticalPadding) + : mName(name) + , mVPadding(verticalPadding) + { + } + }; + std::vector mItems; int mItemHeight; // height of all items }; diff --git a/docs/requirements.txt b/docs/requirements.txt index f46cc5c95d..d22bcceae6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,5 @@ parse_cmake sphinx==7.1.2 docutils==0.18.1 jinja2==3.1.6 -sphinx_rtd_theme==3.0.1 +sphinx-design==0.5.0 +git+https://github.com/glassmancody/sphinxawesome-theme.git@openmw \ No newline at end of file diff --git a/docs/source/_ext/omw-directives.py b/docs/source/_ext/omw-directives.py new file mode 100644 index 0000000000..245c84ad7a --- /dev/null +++ b/docs/source/_ext/omw-directives.py @@ -0,0 +1,76 @@ +from docutils import nodes +from docutils.parsers.rst import Directive, directives + +class OMWSettingDirective(Directive): + has_content = True + option_spec = { + 'title': directives.unchanged_required, + 'type': directives.unchanged_required, + 'range': directives.unchanged, + 'default': directives.unchanged, + 'location': directives.unchanged, + } + + badge_map = { + 'float32': 'bdg-secondary', 'float64': 'bdg-muted', + 'int': 'bdg-primary', 'uint': 'bdg-light', + 'string': 'bdg-success', 'boolean': 'bdg-warning', + 'color': 'bdg-info', + } + + def run(self): + opts = self.options + title = opts['title'] + type_tags = opts['type'].split('|') + badges = ' '.join(f':{self.badge_map.get(t)}:`{t}`' for t in type_tags) + + values = [ + badges, + opts.get('range', ''), + opts.get('default', ''), + opts.get('location', ':bdg-danger:`user settings.cfg`') + ] + + table = nodes.table(classes=['omw-setting']) + tgroup = nodes.tgroup(cols=4) + table += tgroup + for _ in range(4): + tgroup += nodes.colspec(colwidth=20) + + thead = nodes.thead() + tgroup += thead + thead += nodes.row('', *[ + nodes.entry('', nodes.paragraph(text=label)) + for label in ['Type', 'Range', 'Default', 'Location'] + ]) + + tbody = nodes.tbody() + tgroup += tbody + row = nodes.row() + + for i, val in enumerate(values): + entry = nodes.entry() + inline, _ = self.state.inline_text(val, self.lineno) + if i == 2 and 'color' in type_tags: + rgba = [float(c) for c in opts['default'].split()] + style = f'background-color:rgba({rgba[0]*255}, {rgba[1]*255}, {rgba[2]*255}, {rgba[3]})' + chip = nodes.raw('', f'', format='html') + wrapper = nodes.container(classes=['type-color-wrapper'], children=[chip] + inline) + entry += wrapper + else: + entry += inline + row += entry + tbody += row + + desc = nodes.paragraph() + self.state.nested_parse(self.content, self.content_offset, desc) + + section = nodes.section(ids=[nodes.make_id(title)]) + section += nodes.title(text=title) + section += table + section += desc + return [section] + +def setup(app): + app.add_css_file("omw-directives.css") + app.add_directive("omw-setting", OMWSettingDirective) 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 aa83013def..96b7de9bb3 100644 --- a/docs/source/_static/luadoc.css +++ b/docs/source/_static/luadoc.css @@ -1,113 +1,91 @@ -#luadoc tt { font-family: monospace; } - -#luadoc p, -#luadoc td, -#luadoc th { font-size: .95em; line-height: 1.2em;} - -#luadoc p, -#luadoc ul -{ margin: 10px 0 0 10px;} - -#luadoc strong { font-weight: bold;} - -#luadoc em { font-style: italic;} - -#luadoc h1 { - font-size: 1.5em; - margin: 25px 0 20px 0; -} -#luadoc h2, -#luadoc h3, -#luadoc h4 { margin: 15px 0 10px 0; } -#luadoc h2 { font-size: 1.25em; } -#luadoc h3 { font-size: 1.15em; } -#luadoc h4 { font-size: 1.06em; } - -#luadoc hr { - color:#cccccc; - background: #00007f; - height: 1px; -} - -#luadoc blockquote { margin-left: 3em; } - -#luadoc ul { list-style-type: disc; } - -#luadoc p.name { - font-family: "Andale Mono", monospace; - padding-top: 1em; -} - -#luadoc p:first-child { - margin-top: 0px; -} - -#luadoc table.function_list { - border-width: 1px; - border-style: solid; - border-color: #cccccc; - border-collapse: collapse; -} -#luadoc table.function_list td { - border-width: 1px; - padding: 3px; - border-style: solid; - border-color: #cccccc; -} - -#luadoc table.function_list td.name { background-color: #f0f0f0; } -#luadoc table.function_list td.summary { width: 100%; } - -#luadoc dl.table dt, -#luadoc dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} -#luadoc dl.table dd, -#luadoc dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} -#luadoc dl.table h3, -#luadoc dl.function h3 {font-size: .95em;} - - - -#luadoc pre.example { - background-color: #eeffcc; - border: 1px solid #e1e4e5; - padding: 10px; - margin: 10px 0 10px 0; - overflow-x: auto; -} - -#luadoc code { - background-color: inherit; - color: inherit; - border: none; - font-family: monospace; +#luadoc code, #luadoc tt, #luadoc p em { + font-family: 'JetBrains Mono', monospace; } #luadoc pre.example code { - color: #404040; - background-color: #eeffcc; - border: none; white-space: pre; - padding: 0px; } -#luadoc dt { - background: inherit; +#luadoc code:not([class*="language-"]) { + background-color: unset; +} + +#luadoc pre:not([class*="language-"]) { + background-color: inherit; color: inherit; - width: 100%; - padding: 0px; + border: none; } -#luadoc a:not(:link) { - font-weight: bold; - color: #000; +#luadoc pre code { + white-space: normal; +} + +html:has(#luadoc) { + scroll-padding-top: 4.0rem; +} + +#content #luadoc code a:not(.toc-backref) { + font-weight: 600; +} + +#luadoc > p:nth-child(1) { + margin: 12px 0 12px 0; +} + +#luadoc a:not([href]) { text-decoration: none; cursor: inherit; + color: inherit; } -#luadoc a:link { font-weight: bold; color: #004080; text-decoration: none; } -#luadoc a:visited { font-weight: bold; color: #006699; text-decoration: none; } -#luadoc a:link:hover { text-decoration: underline; } -#luadoc dl, -#luadoc dd {margin: 0px; line-height: 1.2em;} +#luadoc a:not([href]):hover { + color: inherit; +} + +#luadoc code a em { + color: inherit; + font-weight: inherit; +} + +#luadoc em { + font-size: 13px; + font-style: normal; +} + +.context-wrapper { + display: flex; + margin-top: 14px; + gap: 4px; +} + +#luadoc ul { list-style-type: disc; } #luadoc li {list-style: bullet;} +#luadoc .function a:not(.toc-backref) { + font-weight: 500; +} + +#content #luadoc dl.function dt:not(.sig):first-child { + border-bottom: 2px solid hsl(var(--muted) / 50%); + background-color: hsl(var(--muted)); + padding: 6px; +} + +#content #luadoc dl.function dt:not(.sig):first-child a { + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + #content #luadoc dl.function dt:not(.sig):first-child { + border-bottom: 2px solid hsl(var(--accent) / 50%); + background-color: hsl(var(--accent)); + } +} + +#luadoc .table-wrapper.container { + padding: 0; +} + +table.docutils { + width: 100%; +} \ No newline at end of file diff --git a/docs/source/_static/omw-directives.css b/docs/source/_static/omw-directives.css new file mode 100644 index 0000000000..38f6218025 --- /dev/null +++ b/docs/source/_static/omw-directives.css @@ -0,0 +1,21 @@ +.omw-setting td:nth-child(1) { width: 20%; } +.omw-setting td:nth-child(2) { width: 20%; } +.omw-setting td:nth-child(3) { width: 20%; } +.omw-setting td:nth-child(4) { width: 20%; } + +.omw-setting .type-color-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: left; + padding: 0; + gap: 12px; +} + +.color-chip { + display: inline-block; + width: 1.5em; + height: 1.5em; + border-radius: 0.2em; + border: 1px solid #333; +} diff --git a/docs/source/_static/prism-dark.css b/docs/source/_static/prism-dark.css new file mode 100644 index 0000000000..293a0d1267 --- /dev/null +++ b/docs/source/_static/prism-dark.css @@ -0,0 +1,156 @@ +@media (prefers-color-scheme: dark) { + + /** + * Github Dark theme for Prism.js + * Based on Github: https://github.com + * @author Katorly + */ + /* General */ + pre[class*="language-"], + code[class*="language-"] { + color: #c9d1d9; + font-size: 13px; + text-shadow: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + } + + pre[class*="language-"]::selection, + code[class*="language-"]::selection, + pre[class*="language-"]::mozselection, + code[class*="language-"]::mozselection { + text-shadow: none; + background: #234879; + } + + @media print { + + pre[class*="language-"], + code[class*="language-"] { + text-shadow: none; + } + } + + pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + background: #161b22; + } + + :not(pre)>code[class*="language-"] { + padding: .1em .3em; + border-radius: .3em; + color: #c9d1d9; + background: #343942; + } + + /* Line highlighting */ + pre[data-line] { + position: relative; + } + + pre[class*="language-"]>code[class*="language-"] { + position: relative; + z-index: 1; + } + + .line-highlight { + position: absolute; + left: 0; + right: 0; + padding: inherit 0; + margin-top: 1em; + background: #2f2a1e; + box-shadow: inset 5px 0 0 #674c16; + z-index: 0; + pointer-events: none; + line-height: inherit; + white-space: pre; + } + + /* Tokens */ + .namespace { + opacity: .7; + } + + .token.comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: #8b949e; + } + + .token.punctuation { + color: #c9d1d9; + } + + .token.property, + .token.tag, + .token.boolean, + .token.number, + .token.constant, + .token.symbol, + .token.deleted { + color: #79c0ff; + } + + .token.selector, + .token.attr-name, + .token.string, + .token.char, + .token.builtin, + .token.inserted { + color: #a5d6ff; + } + + .token.operator, + .token.entity, + .token.url, + .language-css .token.string, + .style .token.string { + color: #a5d6ff; + background: #161b22; + } + + .token.atrule, + .token.attr-value, + .token.keyword { + color: #a5d6ff; + } + + .token.function { + color: #d2a8ff; + } + + .token.regex, + .token.important, + .token.variable { + color: #a8daff; + } + + .token.important, + .token.bold { + font-weight: bold; + } + + .token.italic { + font-style: italic; + } + + .token.entity { + cursor: help; + } +} \ No newline at end of file diff --git a/docs/source/_static/prism.css b/docs/source/_static/prism.css new file mode 100644 index 0000000000..088a96cb6c --- /dev/null +++ b/docs/source/_static/prism.css @@ -0,0 +1,152 @@ +/** + * Github Light theme for Prism.js + * Based on Github: https://github.com + * @author Katorly + */ +/* General */ +pre[class*="language-"], +code[class*="language-"] { + color: #24292f; + font-size: 13px; + text-shadow: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::selection, +code[class*="language-"]::selection, +pre[class*="language-"]::mozselection, +code[class*="language-"]::mozselection { + text-shadow: none; + background: #9fc6e9; +} + +@media print { + + pre[class*="language-"], + code[class*="language-"] { + text-shadow: none; + } +} + +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + background: #f6f8fa; +} + +:not(pre)>code[class*="language-"] { + padding: .1em .3em; + border-radius: .3em; + color: #24292f; + background: #eff1f3; +} + +/* Line highlighting */ +pre[data-line] { + position: relative; +} + +pre[class*="language-"]>code[class*="language-"] { + position: relative; + z-index: 1; +} + +.line-highlight { + position: absolute; + left: 0; + right: 0; + padding: inherit 0; + margin-top: 1em; + background: #fff8c5; + box-shadow: inset 5px 0 0 #eed888; + z-index: 0; + pointer-events: none; + line-height: inherit; + white-space: pre; +} + +/* Tokens */ +.namespace { + opacity: .7; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #6e7781; +} + +.token.punctuation { + color: #24292f; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #0550ae; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #0a3069; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #0550ae; +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #cf222e; +} + +.token.function { + color: #8250df; +} + +.token.regex, +.token.important, +.token.variable { + color: #0a3069; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} \ No newline at end of file diff --git a/docs/source/_static/prism.js b/docs/source/_static/prism.js new file mode 100644 index 0000000000..fa92d07899 --- /dev/null +++ b/docs/source/_static/prism.js @@ -0,0 +1,7 @@ +/* PrismJS 1.30.0 +https://prismjs.com/download#themes=prism&languages=json+lua+yaml&plugins=toolbar */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(Cg.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; +Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/}; +!function(e){var n=/[*&][^\s[\]{},]+/,r=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,t="(?:"+r.source+"(?:[ \t]+"+n.source+")?|"+n.source+"(?:[ \t]+"+r.source+")?)",a="(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*".replace(//g,(function(){return"[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]"})),d="\"(?:[^\"\\\\\r\n]|\\\\.)*\"|'(?:[^'\\\\\r\n]|\\\\.)*'";function o(e,n){n=(n||"").replace(/m/g,"")+"m";var r="([:\\-,[{]\\s*(?:\\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp("([\\-:]\\s*(?:\\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)".replace(/<>/g,(function(){return t}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp("((?:^|[:\\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\\s*:\\s)".replace(/<>/g,(function(){return t})).replace(/<>/g,(function(){return"(?:"+a+"|"+d+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:o("\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?"),lookbehind:!0,alias:"number"},boolean:{pattern:o("false|true","i"),lookbehind:!0,alias:"important"},null:{pattern:o("null|~","i"),lookbehind:!0,alias:"important"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o("[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)","i"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(Prism); +!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e=[],t={},n=function(){};Prism.plugins.toolbar={};var a=Prism.plugins.toolbar.registerButton=function(n,a){var r;r="function"==typeof a?a:function(e){var t;return"function"==typeof a.onClick?((t=document.createElement("button")).type="button",t.addEventListener("click",(function(){a.onClick.call(this,e)}))):"string"==typeof a.url?(t=document.createElement("a")).href=a.url:t=document.createElement("span"),a.className&&t.classList.add(a.className),t.textContent=a.text,t},n in t?console.warn('There is a button with the key "'+n+'" registered already.'):e.push(t[n]=r)},r=Prism.plugins.toolbar.hook=function(a){var r=a.element.parentNode;if(r&&/pre/i.test(r.nodeName)&&!r.parentNode.classList.contains("code-toolbar")){var o=document.createElement("div");o.classList.add("code-toolbar"),r.parentNode.insertBefore(o,r),o.appendChild(r);var i=document.createElement("div");i.classList.add("toolbar");var l=e,d=function(e){for(;e;){var t=e.getAttribute("data-toolbar-order");if(null!=t)return(t=t.trim()).length?t.split(/\s*,\s*/g):[];e=e.parentElement}}(a.element);d&&(l=d.map((function(e){return t[e]||n}))),l.forEach((function(e){var t=e(a);if(t){var n=document.createElement("div");n.classList.add("toolbar-item"),n.appendChild(t),i.appendChild(n)}})),o.appendChild(i)}};a("label",(function(e){var t=e.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&t.hasAttribute("data-label")){var n,a,r=t.getAttribute("data-label");try{a=document.querySelector("template#"+r)}catch(e){}return a?n=a.content:(t.hasAttribute("data-url")?(n=document.createElement("a")).href=t.getAttribute("data-url"):n=document.createElement("span"),n.textContent=r),n}})),Prism.hooks.add("complete",r)}}(); diff --git a/docs/source/_static/theme-override.css b/docs/source/_static/theme-override.css new file mode 100644 index 0000000000..25f7bd6f10 --- /dev/null +++ b/docs/source/_static/theme-override.css @@ -0,0 +1,158 @@ +:root { + --link: #4a90e2; + --link-hover: #1c6cd9; + + --readthedocs-search-color: hsl(var(--foreground)); + --readthedocs-search-link-color: var(--link); + --readthedocs-search-content-background-color: hsl(var(--background)); + --readthedocs-search-content-border-color: hsl(var(--border)); + --readthedocs-search-footer-background-color: hsl(var(--background)); + --readthedocs-search-footer-color: hsl(var(--foreground)); + --readthedocs-search-footer-code-background-color: hsl(var(--background)); + --readthedocs-search-footer-code-border-color: hsl(var(--background)); + --readthedocs-search-input-background-color: hsl(var(--accent)); + --readthedocs-search-result-color: hsl(var(--foreground)); + --readthedocs-search-result-icon-color: hsl(var(--foreground)); + --readthedocs-search-result-heading-color: hsl(var(--foreground)); + --readthedocs-search-result-subheading-color: hsl(var(--foreground)); + --readthedocs-search-result-active-background-color: hsl(var(--accent)); + --readthedocs-search-result-border-color: hsl(var(--border)); + + --readthedocs-flyout-background-color: hsl(var(--background)); + --readthedocs-flyout-color: hsl(var(--foreground)); + --readthedocs-flyout-current-version-color: hsl(var(--foreground)); + --readthedocs-flyout-item-link-color: var(--link); + --readthedocs-flyout-link-color: var(--link); + --readthedocs-flyout-section-heading-color: hsl(var(--foreground)); +} + +/* Less aggressive dark background */ +@media (prefers-color-scheme: dark) { + :root { + --background: 220 14% 9% !important; + --border: 216 14% 17%; + --accent: 216 14% 17%; + --input: 216 14% 17%; + --muted: 223 27% 14%; + --card: 220 14% 9%; + + --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 { + --muted-foreground: 215.4 16.3% 86.9%; + } +} + +/* Enable hover-to-highlight in TOC */ +.contents ul li a.reference:hover, .toctree-wrapper ul li a.reference:hover { + text-decoration: underline !important; +} + +/* Hide text underline in tables since the underlines clash with table lines */ +#content a.sd-badge:not(.toc-backref), #content table a:not(.toc-backref) { + text-decoration: none; +} + +/* Override link colors, default is foreground */ +#content a:not(.toc-backref) { + color: var(--link); + font-weight: 600; +} + +#content a:not(.toc-backref):hover, #content a:not(.toc-backref):focus { + color: var(--link-hover); +} + +#content .highlight { + background-color: #f6f8fa !important; + border-radius: 0.25rem; + font-variant-ligatures: none; +} + +@media (prefers-color-scheme: dark) { + #content .highlight { + background-color: #161b22 !important; + } +} + +/* Disables the saturation filter on project icon in dark mode */ +.dark\:invert { + --tw-invert: none !important; +} + +/* Disable no-wrap set on some code blocks, causing x-overflow issues in right bar */ +code { + white-space: normal; +} + +/* Hide the keybind shortcut for search, we haven't linked this to sphinx search addon yet */ +#searchbox kbd { + display: none !important; +} + +#search-input { + padding-right: 12px !important; +} + +/* Table headers are bolded in dark mode, do it in light mode too */ +table th { + font-weight: 600 !important; +} + +/* Highlight non-header rows on hover */ +tbody tr:hover { + background-color: hsl(var(--muted)); +} + +/* Increase maximum width for docs, default is quite narrow at 1400px max */ +@media (min-width: 1400px) { + .container { + max-width: 2000px !important; + } +} + +/* Less intrusive scrollbar in the TOC */ +#left-sidebar::-webkit-scrollbar, #right-sidebar .sticky::-webkit-scrollbar { + width: 6px; + border-radius: 0; +} + +#left-sidebar::-webkit-scrollbar-thumb, #right-sidebar .sticky::-webkit-scrollbar-thumb { + background-color: hsl(var(--border));; + border-radius: 0; +} + +#left-sidebar::-webkit-scrollbar-thumb:hover, #right-sidebar .sticky::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--muted));; +} + +@media (min-width: 1024px) { + .container:has(#left-sidebar) { + grid-template-columns: 310px minmax(0, 1fr); + } +} + +/* Always enable scrollbar in left TOC to prevent layout shifts when expanding */ +#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 fbce7975da..0b953a4275 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,11 +16,16 @@ import os import sys import subprocess +from dataclasses import asdict +from sphinxawesome_theme import ThemeOptions +from sphinxawesome_theme.postprocess import Icons + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. project_root = os.path.abspath('../../') sys.path.insert(0, project_root) +sys.path.append(os.path.abspath("_ext")) # -- General configuration ------------------------------------------------ @@ -37,6 +42,9 @@ extensions = [ 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.autosectionlabel', + 'sphinx_design', + 'omw-directives', + 'omw-lexers', ] #autosectionlabel_prefix_document = True @@ -91,7 +99,14 @@ except Exception as ex: rst_prolog = f""" .. |luaApiRevision| replace:: {luaApiRevision} +.. |luaApiRevisionBadge| replace:: :bdg-link-info-line:`API v{luaApiRevision} ` + .. |ppApiRevision| replace:: {ppApiRevision} +.. |bdg-ctx-menu| replace:: :bdg-warning:`menu` +.. |bdg-ctx-global| replace:: :bdg-danger:`global` +.. |bdg-ctx-player| replace:: :bdg-secondary:`player` +.. |bdg-ctx-local| replace:: :bdg-info:`local` +.. |bdg-ctx-all| replace:: :bdg-danger:`global` :bdg-warning:`menu` :bdg-info:`local` """ # The language for content autogenerated by Sphinx. Refer to documentation @@ -124,7 +139,8 @@ 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. #modindex_common_prefix = [] @@ -138,22 +154,39 @@ primary_domain = 'c' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = 'sphinxawesome_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - 'navigation_with_keys': True, - 'flyout_display': 'attached', -} +html_theme_options = asdict(ThemeOptions( + show_breadcrumbs=False, + main_nav_links= { + "Modding": "reference/modding/index", + "Lua API": "reference/lua-scripting/overview", + "Post-Processing": "reference/postprocessing/index", + }, + show_scrolltop=False, # Useful, but hard to position with the Flyout addon since the best place for that is bottom right +)) + +html_permalinks_icon = Icons.permalinks_icon + +html_js_files = [ + 'prism.js' +] + +html_css_files = [ + "theme-override.css", + "luadoc.css", + "figures.css", + "prism.css", + "prism-dark.css", +] # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] def setup(app): - app.add_css_file('figures.css') - app.add_css_file('luadoc.css') try: subprocess.call(['bash', project_root + '/docs/source/generate_luadoc.sh']) except Exception as e: @@ -161,19 +194,19 @@ def setup(app): # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +html_title = 'OpenMW' # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = 'OpenMW Documentation' +# html_short_title = 'OpenMW Documentation' # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = 'https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/_static/images/openmw.png' +html_logo = 'https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/_static/images/openmw.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = 'https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/_static/images/favicon.png' +html_favicon = 'https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/_static/images/favicon.png' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/source/index.rst b/docs/source/index.rst index de3773ddbd..6906403e9d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,9 +1,34 @@ Welcome to OpenMW's Documentation! ================================== -.. toctree:: - :caption: Table of Contents - :maxdepth: 3 +This documentation covers all aspects of OpenMW development, scripting, and content creation. +Use the categorized sections below to quickly access technical references, modding tools, and installation guides. - manuals/index - reference/index +.. dropdown:: Table of Contents + :icon: book + + .. toctree:: + :caption: Reference + :titlesonly: + :maxdepth: 4 + + reference/modding/index + reference/postprocessing/index + + .. toctree:: + :caption: Lua + :titlesonly: + :maxdepth: 4 + + reference/lua-scripting/overview + reference/lua-scripting/api + reference/lua-scripting/teal + + .. toctree:: + :caption: Help + :titlesonly: + :maxdepth: 4 + + manuals/installation/index + manuals/openmw-cs/index + manuals/documentationHowTo diff --git a/docs/source/install_luadocumentor_in_docker.sh b/docs/source/install_luadocumentor_in_docker.sh index 5d37a37351..388ba8b739 100755 --- a/docs/source/install_luadocumentor_in_docker.sh +++ b/docs/source/install_luadocumentor_in_docker.sh @@ -33,7 +33,7 @@ PATH=$PATH:~/luarocks/bin echo "Install openmwluadocumentor" git clone https://gitlab.com/ptmikheev/openmw-luadocumentor.git cd openmw-luadocumentor -git checkout 78577b255d19a1f4f4f539662e00357936b73c33 +git checkout 122e4f55c5f2dd62076135211e03edfb8dec3a55 luarocks make luarocks/openmwluadocumentor-0.2.0-1.rockspec cd ~ rm -r openmw-luadocumentor diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/manuals/documentationHowTo.rst similarity index 99% rename from docs/source/reference/documentationHowTo.rst rename to docs/source/manuals/documentationHowTo.rst index d2b67d02ca..fb1062cd19 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/manuals/documentationHowTo.rst @@ -1,6 +1,6 @@ -####################################### -So you want to help with documentation? -####################################### +############### +Help with docs? +############### Or a beginner's guide to writing docs without having to deal with more techie stuff than you have to. ##################################################################################################### diff --git a/docs/source/manuals/index.rst b/docs/source/manuals/index.rst deleted file mode 100644 index e6f0cbef2e..0000000000 --- a/docs/source/manuals/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -User Manuals -============ - -.. toctree:: - :maxdepth: 2 - - openmw-cs/index - installation/index diff --git a/docs/source/manuals/installation/index.rst b/docs/source/manuals/installation/index.rst index 6e6f5034ef..c1113603dc 100644 --- a/docs/source/manuals/installation/index.rst +++ b/docs/source/manuals/installation/index.rst @@ -4,9 +4,12 @@ Installation Guide In order to use OpenMW, you must install both the engine and the game files for a compatible game. -.. toctree:: - :maxdepth: 2 +.. dropdown:: Table of Contents + :icon: book - install-openmw - install-game-files - common-problems \ No newline at end of file + .. toctree:: + :maxdepth: 2 + + install-openmw + install-game-files + common-problems diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 42d4eb8d78..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. @@ -163,7 +163,10 @@ Debian/Ubuntu - using "Steam Proton" & "OpenMW launcher". #. Launch "OpenMW launcher" and follow the setup wizard, when asked, point it at the location you installed Morrowind to, we will be looking for the directory that contains the Morrowing.esm file, for example '/steam library/steamapps/common/Morrowind/Data Files/'. #. Everything should now be in place, click that big "PLAY" button and fire up OpenMW. -Note, Bloodmoon.esm needs to be below Tribunal.esm in your datafiles list, if you don't have the right order a red "!" will apear next to the filename in the datafiles section of the OpenMW launcher, just drag bloodmoon below tribunal to fix it. +.. note:: + `Bloodmoon.esm` needs to be below `Tribunal.esm` in your data files list. + If you don't have the right order a red `!` will apear next to the filename in the data files section of the OpenMW launcher. + To fix, drag `Bloodmoon.esm` below `Tribunal.esm`. Wine ~~~~ diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst index 2ef72abfd5..c99b04da91 100644 --- a/docs/source/manuals/installation/install-openmw.rst +++ b/docs/source/manuals/installation/install-openmw.rst @@ -2,53 +2,59 @@ Install OpenMW ============== -The (easier) Binary Way -======================= +Direct Download +=============== If you're not sure what any of the different methods mean, you should probably stick to this one. Simply download the latest version for your operating system from `github.com/OpenMW/openmw/releases `_ and run the install package once downloaded. It's now installed! - .. note:: - There is no need to uninstall previous versions - as OpenMW automatically installs into a separate directory for each new version. - Your saves and configuration are compatible and accessible between versions. +.. note:: + There is no need to uninstall previous versions + as OpenMW automatically installs into a separate directory for each new version. + Your saves and configuration are compatible and accessible between versions. -The (bleeding edge) Source Way -============================== +From Source +=========== Visit the `Development Environment Setup `_ section of the Wiki for detailed instructions on how to build the engine. -The Ubuntu Way -============== +Ubuntu +====== A `Launchpad PPA `_ is available. -Add it and install OpenMW:: +Add it and install OpenMW. + +.. code-block:: console $ sudo add-apt-repository ppa:openmw/openmw $ sudo apt update $ sudo apt install openmw -The Arch Linux Way -================== +Arch Linux +========== The binary package is available in the official [community] Repositories. -To install, simply run the following as root (or in sudo):: +To install, simply run the following as root (or in sudo). - # pacman -S openmw +.. code-block:: console -The Void Linux Way -================== + $ pacman -S openmw + +Void Linux +========== The binary package is available in the official Repository -To install simply run the following as root (or in sudo):: +To install simply run the following as root (or in sudo). - # xbps-install openmw +.. code-block:: console -The Debian Way -============== + $ xbps-install openmw + +Debian +====== OpenMW is available from the unstable (sid) repository of Debian contrib and can be easily installed if you are using testing or unstable. @@ -56,10 +62,11 @@ However, it depends on several packages which are not in stable, so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. This is not recommended or supported. -The Flatpak Way -=============== +Flatpak +======= OpenMW is available as a flatpak. With flatpak installed, run the command below. It should show up on your desktop. -:: - # flatpak install openmw +.. code-block:: console + + $ flatpak install openmw diff --git a/docs/source/manuals/openmw-cs/index.rst b/docs/source/manuals/openmw-cs/index.rst index f1f51409d4..637f5858b2 100644 --- a/docs/source/manuals/openmw-cs/index.rst +++ b/docs/source/manuals/openmw-cs/index.rst @@ -1,33 +1,35 @@ -OpenMW CS User Manual +OpenMW-CS User Manual ##################### -The following document is the complete user manual for *OpenMW CS*, the +The following document is the complete user manual for *OpenMW-CS*, the construction set for the OpenMW game engine. It is intended to serve both as an introduction and a reference for the application. Even if you are familiar with modding *The Elder Scrolls III: Morrowind* you should at least read the first few chapters to familiarise yourself with the new interface. .. warning:: - OpenMW CS is still software in development. The manual does not cover any of + OpenMW-CS is still software in development. The manual does not cover any of its shortcomings, it is written as if everything was working as intended. Please report any software problems as bugs in the software, not errors in the manual. -.. toctree:: - :caption: Table of Contents - :maxdepth: 2 +.. dropdown:: Table of Contents + :icon: book - foreword - tour - files-and-directories - starting-dialog - tables - tables-file - tables-world - tables-mechanics - tables-characters - tables-assets - record-types - record-filters - cell-view - records-drag-and-drop + .. toctree:: + :maxdepth: 2 + + foreword + tour + files-and-directories + starting-dialog + tables + tables-file + tables-world + tables-mechanics + tables-characters + tables-assets + record-types + record-filters + cell-view + records-drag-and-drop diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst deleted file mode 100644 index 9aa409f784..0000000000 --- a/docs/source/reference/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -################## -Reference Material -################## - -.. toctree:: - :maxdepth: 2 - - modding/index - lua-scripting/index - postprocessing/index - documentationHowTo diff --git a/docs/source/reference/lua-scripting/ai/combat.rst b/docs/source/reference/lua-scripting/ai/combat.rst new file mode 100644 index 0000000000..06dd9c1452 --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/combat.rst @@ -0,0 +1,36 @@ +Combat +====== + +.. include:: ../version.rst + +Attack another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - cancelOther + - boolean [default=true] + - whether to cancel all other AI packages + * - target + - `GameObject <../openmw_core.html##(GameObject)>`_ [required] + - the actor to attack + +**Examples** + +.. code-block:: Lua + + -- from local script add package to self + local AI = require('openmw.interfaces').AI + AI.startPackage({type='Combat', target=anotherActor}) + + -- via event to any actor + actor:sendEvent('StartAIPackage', {type='Combat', target=anotherActor}) diff --git a/docs/source/reference/lua-scripting/ai/escort.rst b/docs/source/reference/lua-scripting/ai/escort.rst new file mode 100644 index 0000000000..fd9f2d05e5 --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/escort.rst @@ -0,0 +1,49 @@ +Escort +====== + +.. include:: ../version.rst + +Escort another actor to the given location. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - cancelOther + - boolean [default=true] + - whether to cancel all other AI packages + * - target + - `GameObject <../openmw_core.html##(GameObject)>`_ [required] + - the actor to follow + * - destPosition + - `3d vector <../openmw_util.html##(Vector3)>`_ [required] + - the destination point + * - destCell + - Cell [optional] + - the destination cell + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) + +**Example** + +.. code-block:: Lua + + actor:sendEvent('StartAIPackage', { + type = 'Escort', + target = object.self, + destPosition = util.vector3(x, y, z), + duration = 3 * time.hour, + isRepeat = true + }) diff --git a/docs/source/reference/lua-scripting/ai/follow.rst b/docs/source/reference/lua-scripting/ai/follow.rst new file mode 100644 index 0000000000..ac6b70bcf7 --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/follow.rst @@ -0,0 +1,37 @@ +Follow +====== + +.. include:: ../version.rst + +Follow another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - cancelOther + - boolean [default=true] + - whether to cancel all other AI packages + * - target + - `GameObject <../openmw_core.html##(GameObject)>`_ [required] + - the actor to follow + * - destCell + - Cell [optional] + - the destination cell + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + * - destPosition + - `3d vector <../openmw_util.html##(Vector3)>`_ [optional] + - the destination point + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) diff --git a/docs/source/reference/lua-scripting/ai/pursue.rst b/docs/source/reference/lua-scripting/ai/pursue.rst new file mode 100644 index 0000000000..08385c57ae --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/pursue.rst @@ -0,0 +1,25 @@ +Pursue +====== + +.. include:: ../version.rst + +Pursue another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - cancelOther + - boolean [default=true] + - whether to cancel all other AI packages + * - target + - `GameObject <../openmw_core.html##(GameObject)>`_ [required] + - the actor to pursue diff --git a/docs/source/reference/lua-scripting/ai/travel.rst b/docs/source/reference/lua-scripting/ai/travel.rst new file mode 100644 index 0000000000..06a47c868d --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/travel.rst @@ -0,0 +1,28 @@ +Travel +====== + +.. include:: ../version.rst + +Go to given location. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - cancelOther + - boolean [default=true] + - whether to cancel all other AI packages + * - destPosition + - `3d vector <../openmw_util.html##(Vector3)>`_ [required] + - the point to travel to + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) diff --git a/docs/source/reference/lua-scripting/ai/wander.rst b/docs/source/reference/lua-scripting/ai/wander.rst new file mode 100644 index 0000000000..fc702f12e2 --- /dev/null +++ b/docs/source/reference/lua-scripting/ai/wander.rst @@ -0,0 +1,53 @@ +Wander +====== + +.. include:: ../version.rst + +Wander nearby current position. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - type + - string [required] + - the name of the package (see packages listed below) + * - distance + - float [default=0] + - the actor to follow + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + * - idle + - table [optional] + - Idle chance values, up to 8 + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) + +**Example** + +.. code-block:: Lua + + local idleTable = { + idle2 = 60, + idle3 = 50, + idle4 = 40, + idle5 = 30, + idle6 = 20, + idle7 = 10, + idle8 = 0, + idle9 = 25 + } + actor:sendEvent('StartAIPackage', { + type = 'Wander', + distance = 5000, + duration = 5 * time.hour, + idle = idleTable, + isRepeat = true + }) diff --git a/docs/source/reference/lua-scripting/aipackages.rst b/docs/source/reference/lua-scripting/aipackages.rst deleted file mode 100644 index 7a23d156f5..0000000000 --- a/docs/source/reference/lua-scripting/aipackages.rst +++ /dev/null @@ -1,225 +0,0 @@ -Built-in AI packages -==================== - -.. include:: version.rst - -Starting an AI package ----------------------- - -There are two ways to start AI package: - -.. code-block:: Lua - - -- from local script add package to self - local AI = require('openmw.interfaces').AI - AI.startPackage(options) - - -- via event to any actor - actor:sendEvent('StartAIPackage', options) - -``options`` is Lua table with arguments of the AI package. - -**Common arguments that can be used with any AI package** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - type - - string [required] - - the name of the package (see packages listed below) - * - cancelOther - - boolean [default=true] - - whether to cancel all other AI packages - -Combat ------- - -Attack another actor. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - target - - `GameObject `_ [required] - - the actor to attack - -**Examples** - -.. code-block:: Lua - - -- from local script add package to self - local AI = require('openmw.interfaces').AI - AI.startPackage({type='Combat', target=anotherActor}) - - -- via event to any actor - actor:sendEvent('StartAIPackage', {type='Combat', target=anotherActor}) - -Pursue ------- - -Pursue another actor. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - target - - `GameObject `_ [required] - - the actor to pursue - -Follow ------- - -Follow another actor. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - target - - `GameObject `_ [required] - - the actor to follow - * - destCell - - Cell [optional] - - the destination cell - * - duration - - number [optional] - - duration in game time (will be rounded up to the next hour) - * - destPosition - - `3d vector `_ [optional] - - the destination point - * - isRepeat - - boolean [optional] - - Will the package repeat (true or false) - -Escort ------- - -Escort another actor to the given location. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - target - - `GameObject `_ [required] - - the actor to follow - * - destPosition - - `3d vector `_ [required] - - the destination point - * - destCell - - Cell [optional] - - the destination cell - * - duration - - number [optional] - - duration in game time (will be rounded up to the next hour) - * - isRepeat - - boolean [optional] - - Will the package repeat (true or false) - -**Example** - -.. code-block:: Lua - - actor:sendEvent('StartAIPackage', { - type = 'Escort', - target = object.self, - destPosition = util.vector3(x, y, z), - duration = 3 * time.hour, - isRepeat = true - }) - -Wander ------- - -Wander nearby current position. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - distance - - float [default=0] - - the actor to follow - * - duration - - number [optional] - - duration in game time (will be rounded up to the next hour) - * - idle - - table [optional] - - Idle chance values, up to 8 - * - isRepeat - - boolean [optional] - - Will the package repeat (true or false) - -**Example** - -.. code-block:: Lua - - local idleTable = { - idle2 = 60, - idle3 = 50, - idle4 = 40, - idle5 = 30, - idle6 = 20, - idle7 = 10, - idle8 = 0, - idle9 = 25 - } - actor:sendEvent('StartAIPackage', { - type = 'Wander', - distance = 5000, - duration = 5 * time.hour, - idle = idleTable, - isRepeat = true - }) - -Travel ------- - -Go to given location. - -**Arguments** - -.. list-table:: - :header-rows: 1 - :widths: 20 20 60 - - * - name - - type - - description - * - destPosition - - `3d vector `_ [required] - - the point to travel to - * - isRepeat - - boolean [optional] - - Will the package repeat (true or false) diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 82a860b355..b8920b7ff1 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -1,60 +1,21 @@ -################# -Lua API reference -################# +############# +API Reference +############# .. include:: version.rst .. toctree:: :hidden: - engine_handlers - user_interface - aipackages + index_packages + index_auxpackages + index_aipackages + index_interfaces + UI setting_renderers + Engine Handlers events - openmw_ambient - openmw_animation - openmw_async - openmw_camera - openmw_core - openmw_debug - openmw_input - openmw_markup - openmw_menu - openmw_nearby - openmw_postprocessing - openmw_self - openmw_storage - openmw_types - openmw_ui - openmw_util - openmw_vfs - openmw_world - openmw_aux_calendar - openmw_aux_time - openmw_aux_ui - openmw_aux_util - interface_activation - interface_ai - interface_animation - interface_camera - interface_controls - interface_gamepadcontrols - interface_item_usage - interface_mwui - interface_settings - interface_skill_progression - interface_ui - interface_crimes - iterables - - -- :ref:`Engine handlers reference` -- :ref:`User interface reference ` -- `Game object reference `_ -- `Cell reference `_ -- :ref:`Built-in AI packages` -- :ref:`Built-in events` + Iterables **API packages** @@ -66,7 +27,7 @@ Player scripts are local scripts that are attached to a player. .. include:: tables/packages.rst -**openmw_aux** +**Auxiliary packages** ``openmw_aux.*`` are built-in libraries that are itself implemented in Lua. They can not do anything that is not possible with the basic API, they only make it more convenient. Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can override them, but it is not recommended. diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index ac6979a236..29b14aee55 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -7,6 +7,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Can be defined by any script** +|bdg-ctx-all| + .. list-table:: :widths: 20 80 @@ -16,6 +18,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Can be defined by any non-menu script** +|bdg-ctx-global| |bdg-ctx-local| + .. list-table:: :widths: 20 80 @@ -39,6 +43,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Only for global scripts** +|bdg-ctx-global| + .. list-table:: :widths: 20 80 @@ -61,6 +67,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Only for local scripts** +|bdg-ctx-local| + .. list-table:: :widths: 20 80 @@ -86,6 +94,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Only menu scripts and local scripts attached to a player** +|bdg-ctx-menu| |bdg-ctx-player| + .. list-table:: :widths: 20 80 @@ -140,6 +150,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Only for local scripts attached to a player** +|bdg-ctx-player| + .. list-table:: :widths: 20 80 @@ -152,6 +164,8 @@ Engine handler is a function defined by a script, that can be called by the engi **Only for menu scripts** +|bdg-ctx-menu| + .. list-table:: :widths: 20 80 diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 282e3d1173..15c9b52eea 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -1,5 +1,5 @@ -Built-in events -=============== +Events +====== .. include:: version.rst @@ -41,9 +41,53 @@ 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. + + 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 +135,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/index.rst b/docs/source/reference/lua-scripting/index.rst deleted file mode 100644 index f3764c4401..0000000000 --- a/docs/source/reference/lua-scripting/index.rst +++ /dev/null @@ -1,18 +0,0 @@ -#################### -OpenMW Lua scripting -#################### - -.. note:: - OpenMW Lua is not compatible with MWSE. - -.. include:: version.rst - -.. toctree:: - :caption: Table of Contents - :includehidden: - :maxdepth: 2 - - overview - api - teal - diff --git a/docs/source/reference/lua-scripting/index_aipackages.rst b/docs/source/reference/lua-scripting/index_aipackages.rst new file mode 100644 index 0000000000..2b8b41bb08 --- /dev/null +++ b/docs/source/reference/lua-scripting/index_aipackages.rst @@ -0,0 +1,24 @@ +AI packages +============ + +.. include:: version.rst + +.. toctree:: + :hidden: + :glob: + + ai/* + +Starting an AI package +---------------------- + +There are two ways to start AI package: + +.. code-block:: Lua + + -- from local script add package to self + local AI = require('openmw.interfaces').AI + AI.startPackage(options) + + -- via event to any actor + actor:sendEvent('StartAIPackage', options) diff --git a/docs/source/reference/lua-scripting/index_auxpackages.rst b/docs/source/reference/lua-scripting/index_auxpackages.rst new file mode 100644 index 0000000000..ea17a3ddbb --- /dev/null +++ b/docs/source/reference/lua-scripting/index_auxpackages.rst @@ -0,0 +1,21 @@ +################## +Auxiliary Packages +################## + +.. include:: version.rst + +.. toctree:: + :hidden: + + calendar + time + ui + util + + +**Auxiliary packages** + +``openmw_aux.*`` are built-in libraries that are itself implemented in Lua. They can not do anything that is not possible with the basic API, they only make it more convenient. +Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can override them, but it is not recommended. + +.. include:: tables/aux_packages.rst diff --git a/docs/source/reference/lua-scripting/index_interfaces.rst b/docs/source/reference/lua-scripting/index_interfaces.rst new file mode 100644 index 0000000000..37a2df63c4 --- /dev/null +++ b/docs/source/reference/lua-scripting/index_interfaces.rst @@ -0,0 +1,25 @@ +########## +Interfaces +########## + +.. include:: version.rst + +.. toctree:: + :hidden: + + Activation + AI + AnimationController + Camera + Controls + Crimes + GamepadControls + ItemUsage + MWUI + Settings + SkillProgression + UI + +**Interfaces of built-in scripts** + +.. include:: tables/interfaces.rst diff --git a/docs/source/reference/lua-scripting/index_packages.rst b/docs/source/reference/lua-scripting/index_packages.rst new file mode 100644 index 0000000000..53a836519f --- /dev/null +++ b/docs/source/reference/lua-scripting/index_packages.rst @@ -0,0 +1,37 @@ +######## +Packages +######## + +.. include:: version.rst + +.. toctree:: + :hidden: + + ambient + animation + async + camera + core + debug + input + markup + menu + nearby + postprocessing + self + storage + types + ui + util + vfs + world + +**API packages** + +API packages provide functions that can be called by scripts. I.e. it is a script-to-engine interaction. +A package can be loaded with ``require('')``. +It can not be overloaded even if there is a lua file with the same name. +The list of available packages is different for global and for local scripts. +Player scripts are local scripts that are attached to a player. + +.. include:: tables/packages.rst diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index b889b09a9f..b838385524 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -1,5 +1,5 @@ -Overview of Lua scripting -######################### +Overview +######## .. include:: version.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. @@ -384,8 +386,8 @@ Player scripts are local scripts that are attached to a player. .. include:: tables/packages.rst -openmw_aux ----------- +Auxiliary packages +------------------ ``openmw_aux.*`` are built-in libraries that are themselves implemented in Lua. They can not do anything that is not possible with the basic API, they only make it more convenient. Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can override them, but it is not recommended. @@ -544,7 +546,7 @@ The protection mod attaches an additional local script to every actor. The scrip In order to be able to intercept the event, the protection script should be placed in the load order below the original script. -See :ref:`the list of events ` that are used by built-in scripts. +See :ref:`the list of events ` that are used by built-in scripts. Timers @@ -618,7 +620,7 @@ An example: } } -Also in `openmw_aux`_ is the helper function ``runRepeatedly``, it is implemented on top of unsavable timers: +Also in `Auxiliary packages`_ is the helper function ``runRepeatedly``, it is implemented on top of unsavable timers: .. code-block:: Lua @@ -641,7 +643,7 @@ Using IDE for Lua scripting Find the directory ``resources/lua_api`` in your installation of OpenMW. It describes OpenMW LuaAPI in `LDT Documentation Language `__. -It is the source from which the :ref:`API reference ` is generated. +It is the source from which the :ref:`API reference ` is generated. If you write scripts using `Lua Development Tools `__ (eclipse-based IDE), you can import these files to get code autocompletion and integrated OpenMW API reference. Here are the steps: diff --git a/docs/source/reference/lua-scripting/setting_renderers.rst b/docs/source/reference/lua-scripting/setting_renderers.rst index b85c7fbaab..f315615cb4 100644 --- a/docs/source/reference/lua-scripting/setting_renderers.rst +++ b/docs/source/reference/lua-scripting/setting_renderers.rst @@ -1,5 +1,5 @@ -Built-in Setting Renderers -========================== +Setting Renderers +================= .. include:: version.rst diff --git a/docs/source/reference/lua-scripting/tables/aux_packages.rst b/docs/source/reference/lua-scripting/tables/aux_packages.rst index d0217ce202..202f5219c2 100644 --- a/docs/source/reference/lua-scripting/tables/aux_packages.rst +++ b/docs/source/reference/lua-scripting/tables/aux_packages.rst @@ -1,12 +1,19 @@ -+---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -| Built-in library | Can be used | Description | -+=========================================================+====================+===============================================================+ -|:ref:`openmw_aux.calendar ` | everywhere | | Game time calendar | -+---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | -+---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw_aux.time ` | everywhere | | Timers and game time utils | -+---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw_aux.ui ` | by player and menu | | User interface utils | -| | scripts | | -+---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +.. list-table:: + :widths: 30 40 60 + :header-rows: 1 + + * - Module + - Context + - Description + * - :doc:`calendar ` + - |bdg-ctx-all| + - Game time calendar + * - :doc:`time ` + - |bdg-ctx-all| + - Timers and game time utils + * - :doc:`ui ` + - |bdg-ctx-menu| |bdg-ctx-player| + - User interface utils + * - :doc:`util ` + - |bdg-ctx-all| + - Miscellaneous utils diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index d8dfffe47d..8496d01029 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -1,48 +1,43 @@ .. list-table:: - :widths: 20 20 60 + :widths: 30 40 60 + :header-rows: 1 * - Interface - - Can be used + - Context - Description - * - :ref:`Activation ` - - by global scripts + * - :doc:`Activation ` + - |bdg-ctx-global| - Allows to extend or override built-in activation mechanics. - * - :ref:`AI ` - - by local scripts + * - :doc:`AI ` + - |bdg-ctx-local| - Control basic AI of NPCs and creatures. - * - :ref:`AnimationController ` - - by local scripts + * - :doc:`AnimationController ` + - |bdg-ctx-local| - Control animations of NPCs and creatures. - * - :ref:`Camera ` - - by player scripts - - | Allows to alter behavior of the built-in camera script - | without overriding the script completely. - * - :ref:`Controls ` - - by player scripts - - | Allows to alter behavior of the built-in script - | that handles player controls. - * - :ref:`GamepadControls ` - - by player scripts - - | Allows to alter behavior of the built-in script - | that handles player gamepad controls. - * - :ref:`ItemUsage ` - - by global scripts - - | Allows to extend or override built-in item usage - | mechanics. - * - :ref:`SkillProgression ` - - by local scripts - - | Control, extend, and override skill progression of the - | player. - * - :ref:`Settings ` - - by player, menu, and global scripts - - Save, display and track changes of setting values. - * - :ref:`MWUI ` - - by player and menu scripts - - Morrowind-style UI templates. - * - :ref:`UI ` - - by player scripts - - | High-level UI modes interface. Allows to override parts - | of the interface. - * - :ref:`Crimes ` - - by global scripts + * - :doc:`Camera ` + - |bdg-ctx-player| + - Allows to alter behavior of the built-in camera script without overriding the script completely. + * - :doc:`Controls ` + - |bdg-ctx-player| + - Allows to alter behavior of the built-in script that handles player controls. + * - :doc:`Crimes ` + - |bdg-ctx-global| - Commit crimes. + * - :doc:`GamepadControls ` + - |bdg-ctx-player| + - Allows to alter behavior of the built-in script that handles player gamepad controls. + * - :doc:`ItemUsage ` + - |bdg-ctx-global| + - Allows to extend or override built-in item usage mechanics. + * - :doc:`MWUI ` + - |bdg-ctx-menu| |bdg-ctx-player| + - Morrowind-style UI templates. + * - :doc:`Settings ` + - |bdg-ctx-global| |bdg-ctx-menu| |bdg-ctx-player| + - Save, display and track changes of setting values. + * - :doc:`SkillProgression ` + - |bdg-ctx-local| + - Control, extend, and override skill progression of the player. + * - :doc:`UI ` + - |bdg-ctx-player| + - High-level UI modes interface. Allows to override parts of the interface. diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst index e66926e5e4..2485f2c0cd 100644 --- a/docs/source/reference/lua-scripting/tables/packages.rst +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -1,49 +1,64 @@ -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -| Package | Can be used | Description | -+============================================================+====================+===============================================================+ -|:ref:`openmw.ambient ` | by player and menu | | Controls background sounds for given player. | -| | scripts | | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.animation ` | by local and | | Animation controls | -| | player scripts | | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.async ` | everywhere | | Timers and callbacks. | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.camera ` | by player scripts | | Controls camera. | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.core ` | everywhere | | Functions that are common for both global and local scripts | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.debug ` | by player scripts | | Collection of debug utils. | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.input ` | by player and menu | | User input. | -| | scripts | | -+------------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.interfaces